【发布时间】:2014-02-08 07:46:38
【问题描述】:
TL;DR:StaTaskScheduler 运行的任务内部出现死锁。 长版本:
我正在使用 Parallel Team 的 ParallelExtensionsExtras 中的 StaTaskScheduler 来托管第三方提供的一些旧版 STA COM 对象。 StaTaskScheduler 实现细节的描述如下:
好消息是 TPL 的实现可以在任一平台上运行 MTA 或 STA 线程,并考虑到相关差异 WaitHandle.WaitAll 等底层 API(仅支持 MTA 当方法提供多个等待句柄时线程)。
我认为这意味着 TPL 的阻塞部分将使用等待 API 来泵送消息,例如 CoWaitForMultipleHandles,以避免在 STA 线程上调用时出现死锁情况。
在我的情况下,我相信正在发生以下情况:进程内 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> 内部某处的阻塞等待不会发送消息,所以我对引用语句的假设可能是错误的。
已编辑 在测试 WinForms 应用程序的 UI 线程上执行相同的代码时(即提供 TaskScheduler.FromCurrentSynchronizationContext() 而不是 staTaskScheduler 到 Task.Factory.StartNew)。
解决这个问题的正确方法是什么?我是否应该实现一个自定义同步上下文,它将使用CoWaitForMultipleHandles 显式泵送消息,并将其安装在StaTaskScheduler 启动的每个STA 线程上?
如果是这样,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 退出【问题讨论】:
-
@StephenCleary,我确实意识到这是关于底层
WaitHandle.Wait而不是 TPL。我用一些示例代码编辑了这个问题,显示了 STA 线程不泵的情况。我的代码有错误吗? -
@avo,在您描述的场景中,当 COM 对对象
B进行进程外调用并等待它返回时,应该会发生泵送。在这种情况下,B应该能够回拨A。问题一定是别的。 -
此外,您发布的示例使用常规 Windows 消息,
CoWaitForMultipleHandles可能会忽略该消息。文档说:STA 中的默认值只是发送了一小部分特殊情况的消息(即,没有COWAIT_DISPATCH_WINDOW_MESSAGES)。根据@StephenCleary 发布的link,CLR 确实将CoWaitForMultipleHandles用于托管STA 线程,但可能没有COWAIT_DISPATCH_WINDOW_MESSAGES。 -
@avo,你可能对this感兴趣。
标签: c# .net com task-parallel-library async-await