StaTaskScheduler y bombeo de mensajes del hilo STA
TL; DR:Un punto muerto en una tarea ejecutada porStaTaskScheduler
. Versión larga:
Estoy usandoStaTaskScheduler
desdeParaleloExtensionesExtras por Parallel Team, para alojar algunos objetos STA COM heredados suministrados por un tercero. La descripción de laStaTaskScheduler
detalles de implementación dice lo siguiente:
La buena noticia es que la implementación de TPL puede ejecutarse en subprocesos MTA o STA, y tiene en cuenta las diferencias relevantes en torno a las API subyacentes como WaitHandle.WaitAll (que solo admite subprocesos MTA cuando se proporciona el método con múltiples controles de espera).
Pensé que eso significaría que las partes bloqueadas de TPL utilizarían una API de espera que bombea mensajes, comoCoWaitForMultipleHandles
, para evitar situaciones de interbloqueo cuando se invoca en un subproceso STA.
En mi situación, creo que está ocurriendo lo siguiente: el objeto A de STA COM en el proceso realiza una llamada al objeto B de fuera de proceso, luego espera una devolución de llamada de B a través de la llamada saliente.
En una forma simplificada:
var result = await Task.Factory.StartNew(() =>
{
// in-proc object A
var a = new A();
// out-of-proc object B
var b = new B();
// A calls B and B calls back A during the Method call
return a.Method(b);
}, CancellationToken.None, TaskCreationOptions.None, staTaskScheduler);
El problema es,a.Method(b)
nunca vuelve Por lo que puedo decir, esto sucede porque una espera de bloqueo en algún lugar dentroBlockingCollection<Task>
no bombea mensajes, por lo que mi suposición sobre la declaración citada es probablemente incorrecta.
Editado El mismo código funciona cuando se ejecuta en el subproceso de la interfaz de usuario de la aplicación WinForms de prueba (es decir, proporcionaTaskScheduler.FromCurrentSynchronizationContext()
en lugar destaTaskScheduler
aTask.Factory.StartNew
).
¿Cuál es la forma correcta de resolver esto? ¿Debería implementar un contexto de sincronización personalizado, que bombearía explícitamente mensajes conCoWaitForMultipleHandles
, e instálalo en cada hilo STA iniciado porStaTaskScheduler
?
Si es así, ¿la implementación subyacente deBlockingCollection
estar llamando a miSynchronizationContext.Wait
¿método? Puedo usarSynchronizationContext.WaitHelper
para implementarSynchronizationContext.Wait
?
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleTestApp
{
class Program
{
// start and run an STA thread
static void RunStaThread(bool pump)
{
// test a blocking wait with BlockingCollection.Take
var tasks = new BlockingCollection<Task>();
var thread = new Thread(() =>
{
// Create a simple Win32 window
var hwndStatic = NativeMethods.CreateWindowEx(0, "Static", String.Empty, NativeMethods.WS_POPUP,
0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
// subclass it with a custom WndProc
IntPtr prevWndProc = IntPtr.Zero;
var newWndProc = new NativeMethods.WndProc((hwnd, msg, wParam, lParam) =>
{
if (msg == NativeMethods.WM_TEST)
Console.WriteLine("WM_TEST processed");
return NativeMethods.CallWindowProc(prevWndProc, hwnd, msg, wParam, lParam);
});
prevWndProc = NativeMethods.SetWindowLong(hwndStatic, NativeMethods.GWL_WNDPROC, newWndProc);
if (prevWndProc == IntPtr.Zero)
throw new ApplicationException();
// post a test WM_TEST message to it
NativeMethods.PostMessage(hwndStatic, NativeMethods.WM_TEST, IntPtr.Zero, IntPtr.Zero);
// BlockingCollection blocks without pumping, NativeMethods.WM_TEST never arrives
try { var task = tasks.Take(); }
catch (Exception e) { Console.WriteLine(e.Message); }
if (pump)
{
// NativeMethods.WM_TEST will arrive, because Win32 MessageBox pumps
Console.WriteLine("Now start pumping...");
NativeMethods.MessageBox(IntPtr.Zero, "Pumping messages, press OK to stop...", String.Empty, 0);
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
Thread.Sleep(2000);
// this causes the STA thread to end
tasks.CompleteAdding();
thread.Join();
}
static void Main(string[] args)
{
Console.WriteLine("Testing without pumping...");
RunStaThread(false);
Console.WriteLine("\nTest with pumping...");
RunStaThread(true);
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
}
}
// Interop
static class NativeMethods
{
[DllImport("user32")]
public static extern IntPtr SetWindowLong(IntPtr hwnd, int nIndex, WndProc newProc);
[DllImport("user32")]
public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hwnd, int msg, int wParam, int lParam);
[DllImport("user32.dll")]
public static extern IntPtr CreateWindowEx(int dwExStyle, string lpClassName, string lpWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
[DllImport("user32.dll")]
public static extern bool PostMessage(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern int MessageBox(IntPtr hwnd, string text, String caption, int options);
public delegate IntPtr WndProc(IntPtr hwnd, int msg, int wParam, int lParam);
public const int GWL_WNDPROC = -4;
public const int WS_POPUP = unchecked((int)0x80000000);
public const int WM_USER = 0x0400;
public const int WM_TEST = WM_USER + 1;
}
}
Esto produce la salida:
Testing without pumping... The collection argument is empty and has been marked as complete with regards to additions. Test with pumping... The collection argument is empty and has been marked as complete with regards to additions. Now start pumping... WM_TEST processed Press Enter to exit