TL;DR: 在由StaTaskScheduler
运行的任务中出现死锁。 长篇版本:
我正在使用Parallel Team的ParallelExtensionsExtras中的StaTaskScheduler
,以托管第三方提供的一些遗留STA COM对象。 StaTaskScheduler
实现细节的描述如下:
好消息是TPL的实现能够在MTA或STA线程上运行,并且考虑了底层API(例如WaitHandle.WaitAll,在提供多个等待句柄时仅支持MTA线程)的相关差异。
我认为这意味着TPL的阻塞部分将使用一种消息泵等待API(例如CoWaitForMultipleHandles
),以避免在STA线程上调用时出现死锁情况。
简化形式如下:
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>
中的某个阻塞等待没有泵送消息,因此我对引用语句的假设可能是错误的。
编辑 当在测试 WinForms 应用程序的 UI 线程上执行相同的代码时(即向 Task.Factory.StartNew
提供 TaskScheduler.FromCurrentSynchronizationContext()
而不是 staTaskScheduler
),它可以正常工作。应该如何解决这个问题?我应该实现一个自定义同步上下文,在每个由
StaTaskScheduler
启动的 STA 线程上显式地使用 CoWaitForMultipleHandles
泵送消息并安装它吗?如果是这样,
BlockingCollection
的底层实现会调用我的 SynchronizationContext.Wait
方法吗?我可以使用 SynchronizationContext.WaitHelper
实现 SynchronizationContext.Wait
吗?
编辑:附带一些代码,展示了当执行阻塞等待时,托管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;
}
}
这将产生以下输出:
在不进行泵送的情况下进行测试... 集合参数为空,并已标记为完成添加。
进行泵送测试... 集合参数为空,并已标记为完成添加。 现在开始泵送... WM_TEST 已处理 按 Enter 键退出
Task
和线程池的实现,而不是StaTaskScheduler
。托管线程确实会进行消息泵处理;请参见此处和此处。所以我不知道为什么你会看到死锁。 - Stephen ClearyWaitHandle.Wait
。我已经编辑了问题,并附上了一些示例代码,展示了STA线程不会泵的情况。我的代码有错误吗? - avoPostMessage
,而CoWaitForMultipleHandles
在WaitHandle.Wait
中没有被调用,就像BlockingCollection
正在等待的那样。我无法访问“A”的源代码以确定这一点。 - avo