TL; DR: 通过运行一个任务内部的僵局StaTaskScheduler
。 龙版本:
我使用StaTaskScheduler
从ParallelExtensionsExtras中通过平行小组,举办由第三方提供的一些遗留STA COM对象。 的描述StaTaskScheduler
实施细则说以下内容:
好消息是,太平人寿实现能够在任MTA或STA线程运行,并考虑到相关的差异背后周围像WaitHandle.WaitAll的(仅支持该方法时提供多个等待句柄MTA线程)的API。
我以为这将意味着TPL的阻挡部将使用等待API,它泵的消息,像CoWaitForMultipleHandles
,以避免死锁情况对STA线程调用时。
在我的情况,我认为正在发生的事情如下:在进程内STA COM对象,使以外的进程内对象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
,它由开始的每个STA线程上安装StaTaskScheduler
?
如果是这样,将底层实现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;
}
}
这将产生输出:
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
Answer 1:
我对你的问题的理解:你正在使用StaTaskScheduler
只来组织你的传统的COM对象的传统的COM STA公寓。 你没有运行的STA线程上的WinForms或WPF核心消息循环StaTaskScheduler
。 也就是说,你没有使用像什么Application.Run
, Application.DoEvents
或Dispatcher.PushFrame
该线程内。 纠正我,如果这是一个错误的假设。
就其本身而言, StaTaskScheduler
不安装就可以创建STA线程的任何同步上下文。 因此,你在的CLR依靠泵给你留言。 我只发现CLR泵上的STA线程,在一个隐含的确认公寓,并在CLR抽克里斯Brumme:
我一直在说,管理上堵塞STA线程调用时将执行“一些抽水”。 那岂不是巨大的确切地知道什么会变得僵硬? 不幸的是,抽是一个黑色的艺术,是超越凡人修真。 在Win2000及以上,我们只是委托给OLE32的CoWaitForMultipleHandles服务。
这表明CLR使用CoWaitForMultipleHandles
内部的STA线程。 此外,MSDN文档的COWAIT_DISPATCH_WINDOW_MESSAGES
标志提到这一点 :
...在STA只派出一小部分特例,消息。
我做的一些研究 ,但未能得到泵WM_TEST
从你的示例代码与CoWaitForMultipleHandles
,我们讨论的是,在评论你的问题。 我的理解是,上述小套特例,消息 实在有限一些COM特定编码的消息,并且不包括任何常规通用的消息像你WM_TEST
。
因此,要回答你的问题:
......如果我实现了自定义同步环境,这将明确地CoWaitForMultipleHandles抽取消息,并通过StaTaskScheduler开始每个STA线程上安装?
是的,我认为,创建一个自定义的同步环境,并覆盖SynchronizationContext.Wait
确实是正确的解决方案。
然而,你应该避免使用CoWaitForMultipleHandles
,并使用MsgWaitForMultipleObjectsEx
代替 。 如果MsgWaitForMultipleObjectsEx
表示有一个在队列中的未决消息,您应该手动泵与它PeekMessage(PM_REMOVE)
和DispatchMessage
。 然后,你应该继续等待句柄,都是一样的内部SynchronizationContext.Wait
电话。
注意: 有之间的微妙但重要的区别 MsgWaitForMultipleObjectsEx
和MsgWaitForMultipleObjects
。 后者不返回,一直阻塞,如果有已经看到在队列中(例如,一个消息PeekMessage(PM_NOREMOVE)
或GetQueueStatus
),但不会被删除。 这不是好抽,因为你的COM对象可能会使用类似的东西PeekMessage
来检查消息队列。 这可能会导致以后MsgWaitForMultipleObjects
在出乎意料的时候阻拦。
OTOH, MsgWaitForMultipleObjectsEx
与MWMO_INPUTAVAILABLE
标志不会有这样的缺点,并会在这种情况下返回。
前一阵子我创建了一个定制版本StaTaskScheduler
( 可用这里ThreadAffinityTaskScheduler
在试图解决一个) 不同的问题 :维护线程以供后续线程关联池await
延续。 如果你使用多个STA COM对象的线程关联性是至关重要的 awaits
。 原来StaTaskScheduler
存在这种行为,只有当其池被限制为1个线程。
所以,我继续做你多一些尝试WM_TEST
情况。 本来,我安装的是标准的一个实例SynchronizationContext
的STA线程类。 该WM_TEST
消息没有得到抽,这是预期。
然后我重写SynchronizationContext.Wait
只是转发给SynchronizationContext.WaitHelper
。 它没有被调用,但仍然没有泵。
最后,我实现了一个功能齐全的消息泵循环,这里是它的核心部分:
// the core loop
var msg = new NativeMethods.MSG();
while (true)
{
// MsgWaitForMultipleObjectsEx with MWMO_INPUTAVAILABLE returns,
// even if there's a message already seen but not removed in the message queue
nativeResult = NativeMethods.MsgWaitForMultipleObjectsEx(
count, waitHandles,
(uint)remainingTimeout,
QS_MASK,
NativeMethods.MWMO_INPUTAVAILABLE);
if (IsNativeWaitSuccessful(count, nativeResult, out managedResult) || WaitHandle.WaitTimeout == managedResult)
return managedResult;
// there is a message, pump and dispatch it
if (NativeMethods.PeekMessage(out msg, IntPtr.Zero, 0, 0, NativeMethods.PM_REMOVE))
{
NativeMethods.TranslateMessage(ref msg);
NativeMethods.DispatchMessage(ref msg);
}
if (hasTimedOut())
return WaitHandle.WaitTimeout;
}
这并不工作, WM_TEST
被抽。 下面是测试的改编版:
public static async Task RunAsync()
{
using (var staThread = new Noseratio.ThreadAffinity.ThreadWithAffinityContext(staThread: true, pumpMessages: true))
{
Console.WriteLine("Initial thread #" + Thread.CurrentThread.ManagedThreadId);
await staThread.Run(async () =>
{
Console.WriteLine("On STA thread #" + Thread.CurrentThread.ManagedThreadId);
// create a simple Win32 window
IntPtr hwnd = CreateTestWindow();
// Post some WM_TEST messages
Console.WriteLine("Post some WM_TEST messages...");
NativeMethods.PostMessage(hwnd, NativeMethods.WM_TEST, new IntPtr(1), IntPtr.Zero);
NativeMethods.PostMessage(hwnd, NativeMethods.WM_TEST, new IntPtr(2), IntPtr.Zero);
NativeMethods.PostMessage(hwnd, NativeMethods.WM_TEST, new IntPtr(3), IntPtr.Zero);
Console.WriteLine("Press Enter to continue...");
await ReadLineAsync();
Console.WriteLine("After await, thread #" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Pending messages in the queue: " + (NativeMethods.GetQueueStatus(0x1FF) >> 16 != 0));
Console.WriteLine("Exiting STA thread #" + Thread.CurrentThread.ManagedThreadId);
}, CancellationToken.None);
}
Console.WriteLine("Current thread #" + Thread.CurrentThread.ManagedThreadId);
}
输出 :
Initial thread #9
On STA thread #10
Post some WM_TEST messages...
Press Enter to continue...
WM_TEST processed: 1
WM_TEST processed: 2
WM_TEST processed: 3
After await, thread #10
Pending messages in the queue: False
Exiting STA thread #10
Current thread #12
Press any key to exit
注意此实现支持的线程关联性(它停留在后线#10 await
)和消息抽水。 完整的源代码包含了可重用部件( ThreadAffinityTaskScheduler
和ThreadWithAffinityContext
),并可用这里自足的控制台应用程序 。 它没有被彻底的测试,所以需要您自担风险使用它。
Answer 2:
STA线程抽的题目是一个大与具有愉快的时间解决僵局很少有程序员。 关于它的开创性论文的作者是克里斯Brumme,上.NET工作的主要聪明的家伙。 你会在找到这个博客帖子 。 不幸的是很短的细节,他并没有超越提的是,CLR确实有点抽的,但没有确切的规则的任何细节。
他在谈论,该代码添加在.NET 2.0中,存在于一个名为MsgWaitHelper内部CLR函数()。 用于.NET 2.0的源代码可通过SSCLI20分布。 非常完整,但不包括用于MsgWaitHelper源()。 相当不寻常。 反编译它,而一个失败的事业,这是非常大的。
有一两件事要采取从他的博客是走重入的危险。 在STA线程泵是危险的,其派遣Windows消息,并得到任意代码时,你的程序是不是在正确的状态,让这样的代码来执行的执行能力。 大部分任何程序员VB6知道什么时候他使用的DoEvents东西()获取在他的代码中的模式循环停止冻结UI。 我写了一个帖子关于其最典型的危险。 MsgWaitHelper()这是否确切类型抽自己的,它不过是究竟什么样的代码,它允许运行非常有选择性。
你可以在运行程序而不附加调试,然后附加一个非托管调试器它做什么你的测试程序中的一些见解。 你会看到它阻塞NtWaitForMultipleObjects()。 我把它一步,并设置PeekMessageW(),断点得到这个堆栈跟踪:
user32.dll!PeekMessageW() Unknown
combase.dll!CCliModalLoop::MyPeekMessage(tagMSG * pMsg, HWND__ * hwnd, unsigned int min, unsigned int max, unsigned short wFlag) Line 2305 C++
combase.dll!CCliModalLoop::PeekRPCAndDDEMessage() Line 2008 C++
combase.dll!CCliModalLoop::FindMessage(unsigned long dwStatus) Line 2087 C++
combase.dll!CCliModalLoop::HandleWakeForMsg() Line 1707 C++
combase.dll!CCliModalLoop::BlockFn(void * * ahEvent, unsigned long cEvents, unsigned long * lpdwSignaled) Line 1645 C++
combase.dll!ClassicSTAThreadWaitForHandles(unsigned long dwFlags, unsigned long dwTimeout, unsigned long cHandles, void * * pHandles, unsigned long * pdwIndex) Line 46 C++
combase.dll!CoWaitForMultipleHandles(unsigned long dwFlags, unsigned long dwTimeout, unsigned long cHandles, void * * pHandles, unsigned long * lpdwindex) Line 120 C++
clr.dll!MsgWaitHelper(int,void * *,int,unsigned long,int) Unknown
clr.dll!Thread::DoAppropriateWaitWorker(int,void * *,int,unsigned long,enum WaitMode) Unknown
clr.dll!Thread::DoAppropriateWait(int,void * *,int,unsigned long,enum WaitMode,struct PendingSync *) Unknown
clr.dll!CLREventBase::WaitEx(unsigned long,enum WaitMode,struct PendingSync *) Unknown
clr.dll!CLREventBase::Wait(unsigned long,int,struct PendingSync *) Unknown
clr.dll!Thread::Block(int,struct PendingSync *) Unknown
clr.dll!SyncBlock::Wait(int,int) Unknown
clr.dll!ObjectNative::WaitTimeout(bool,int,class Object *) Unknown
要注意的是我记录该堆栈跟踪在Windows 8.1中,它的外观上较老的Windows版本有很大不同。 在COM模式循环已经在Windows 8中被大量修修补补,这也是一个非常大的交易对WinRT的程序。 不很了解它,但它似乎有另一个STA命名ASTA线程模型,做了更严格的一种抽的,在加入CoWaitForMultipleObjects供奉()
ObjectNative :: WaitTimeout()是其中BlockingCollection.Take内的SemaphoreSlim.Wait()()方法开始执行CLR代码。 你看它犁地通过内部CLR代码的水平在神话MsgWaitHelper()函数到达,然后切换到臭名昭著的COM模式调度循环。
它做了“错误”的那种在你的程序抽的蝙蝠信号标志是CliModalLoop :: PeekRPCAndDDEMessage()方法调用。 换句话说,它是只考虑那种能张贴到调度跨越公寓边界COM调用特定的内部窗口互操作的消息。 它不会泵是在为自己的窗口消息队列中的消息。
这是可以理解的行为时,Windows只能是绝对确保重入不会杀了你的程序的时候才可以看到你的UI线程处于闲置状态 。 它是空闲时,它泵消息循环本身,到的PeekMessage()或的GetMessage()的调用指示状态。 问题是,你不抽自己。 您违反STA线程的核心合同,就必须泵消息循环。 希望在COM模式循环将做抽你因此闲置的希望。
实际上,你可以解决这个问题,尽管我不建议你这样做。 CLR将它留到应用程序本身的正确构造SynchronizationContext.Current对象来执行等待。 您可以通过导出自己的类创建一个覆盖wait()方法。 调用SetWaitNotificationRequired()方法来说服它应该把它留给你的CLR。 一个不完整版本,演示方法:
class MySynchronizationProvider : System.Threading.SynchronizationContext {
public MySynchronizationProvider() {
base.SetWaitNotificationRequired();
}
public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) {
for (; ; ) {
int result = MsgWaitForMultipleObjects(waitHandles.Length, waitHandles, waitAll, millisecondsTimeout, 8);
if (result == waitHandles.Length) System.Windows.Forms.Application.DoEvents();
else return result;
}
}
[DllImport("user32.dll")]
private static extern int MsgWaitForMultipleObjects(int cnt, IntPtr[] waitHandles, bool waitAll,
int millisecondTimeout, int mask);
}
而在你的线程开始安装:
System.ComponentModel.AsyncOperationManager.SynchronizationContext =
new MySynchronizationProvider();
现在,您会看到越来越派出你的WM_TEST消息。 它调用Application.DoEvents()分派它。 我可以覆盖它使用的PeekMessage +在DispatchMessage但这样会模糊这个代码的危险,最好不要沾的DoEvents()在桌子底下。 你真的在这里扮演一个非常危险的重入游戏。 不要使用此代码。
长话短说,使用StaThreadScheduler的唯一希望是正确的,当它在已经实施的STA合同和泵像一个STA线程应该做的代码使用。 它真正的意思是创可贴的旧代码,你不必奢侈来控制线程的状态。 像在VB6程序启动生命或Office加载任何代码。 尝试了一下它,我不认为它实际上可以工作。 值得注意得是,需要为它应该待的完全asych的可用性消除/等待。
文章来源: StaTaskScheduler and STA thread message pumping