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?

Editado con algún código que muestra que un subproceso STA administrado no bombea cuando se realiza una espera de bloqueo. El código es una aplicación de consola completa lista para copiar / pegar / ejecutar:

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

Respuestas a la pregunta(2)

Su respuesta a la pregunta