TL; DR: тупик внутри задачи, выполняемой StaTaskScheduler
. Длинная версия:
Я использую StaTaskScheduler
от ParallelExtensionsExtras от Parallel Team, чтобы разместить несколько устаревших STA COM объекты, предоставленные третьей стороной. Описание деталей реализации StaTaskScheduler
говорит следующее:
Хорошей новостью является то, что реализация TPL может работать на любом MTA или STA, и учитывает соответствующие различия в базовые API, такие как WaitHandle.WaitAll(который поддерживает только MTA потоки, когда для метода предусмотрены несколько дескрипторов ожидания).
Я думал, что это означало бы, что блокирующие части TPL будут использовать API ожидания, который накачивает сообщения, такие как CoWaitForMultipleHandles
, чтобы избежать ситуаций взаимоблокировки при вызове STA нить.
В моей ситуации, я считаю, что происходит следующее: in-proc STA COM-объект A делает вызов для объекта без компромисса B, затем ожидает обратного вызова из B через часть исходящего вызова.
В упрощенной форме:
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);
Проблема в том, что a.Method(b)
никогда не возвращается. Насколько я могу судить, это происходит потому, что остановка блокировки где-то внутри BlockingCollection<Task>
не передает сообщения, поэтому мое предположение о цитируемом утверждении, вероятно, неверно.
EDITED Тот же код работает, когда выполняется в потоке пользовательского интерфейса тестового приложения WinForms (то есть, < <27 > вместо StaTaskScheduler
до Task.Factory.StartNew
).
Каков правильный способ решить эту проблему? Должен ли я реализовать пользовательский контекст синхронизации, который будет явно передавать сообщения с помощью CoWaitForMultipleHandles
и устанавливать его в каждом потоке STA, начатом с StaTaskScheduler
?
Если да, будет ли базовая реализация BlockingCollection
вызывать метод SynchronizationContext.Wait
? Могу ли я использовать SynchronizationContext.WaitHelper
для реализации SynchronizationContext.Wait
?
EDITED с некоторым кодом, показывающим, что управляемый поток STA не накачивается при выполнении блокировки. Код - это готовое консольное приложение для копирования/вставки/запуска:
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;
}
}
Это приводит к выходу:
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