哪些阻塞操作会导致 STA 线程泵送 COM 消息?
Posted
技术标签:
【中文标题】哪些阻塞操作会导致 STA 线程泵送 COM 消息?【英文标题】:Which blocking operations cause an STA thread to pump COM messages? 【发布时间】:2014-03-01 13:12:38 【问题描述】:在 STA 线程上实例化 COM 对象时,该线程通常必须实现消息泵,以便编组对其他线程的调用(请参阅here)。
可以手动泵送消息,也可以依靠某些但不是全部线程阻塞操作在等待时自动泵送与 COM 相关的消息这一事实。文档通常无助于确定哪个是哪个(请参阅this related question)。
如何确定线程阻塞操作是否会在 STA 上泵送 COM 消息?
到目前为止的部分列表:
阻止进行抽水的操作*:
Thread.Join
WaitHandle.WaitOne
/WaitAny
/WaitAll
(虽然不能从 STA 线程调用WaitAll
)
GC.WaitForPendingFinalizers
Monitor.Enter
(因此是lock
) - 在某些情况下
ReaderWriterLock
BlockingCollection
阻止不抽水的操作:
Thread.Sleep
Console.ReadKey
(在某处阅读)
*注意Noseratio's answer 表示即使是进行泵送的操作,也会针对一组非常有限的未公开的特定于 COM 的消息。
【问题讨论】:
断言“一般来说,COM 对象必须在 STA 上实例化”是不正确的。没有“一般性”也没有“必须”,因为它实际上取决于 COM 对象,它是如何向 COM 声明的。事实上,COM 会为您完成所有工作以避免这些“必须”(有时会以不必要的编组为代价)。 @SimonMourier - 感谢您的更正 - 我会更新问题。 包装看不见的同步对象的类,如 BlockingCollection,属于 WaitHandle.Wait 括号。 @HansPassant - 如何判断包装类使用哪种类型的 sych 对象?例如,文档没有指定BlockingCollection.Take()
实际阻塞的方式。
您可以查看参考源或使用反编译器。这实际上并不重要,对同步对象的任何等待最终都会使用 CLR 中的相同代码。
【参考方案1】:
BlockingCollection
确实会在阻塞时抽水。我在回答以下问题时了解到这一点,其中有一些关于 STA 泵送的有趣细节:
StaTaskScheduler and STA thread message pumping
但是,它将发送一组非常有限的未公开的 COM 特定消息,与您列出的其他 API 相同。它不会发送通用 Win32 消息(一个特殊情况是 WM_TIMER
,也不会发送)。这可能是一些需要全功能消息循环的 STA COM 对象的问题。
如果您想对此进行试验,请创建您自己的 SynchronizationContext
版本,覆盖 SynchronizationContext.Wait
,调用 SetWaitNotificationRequired
并在 STA 线程上安装您的自定义同步上下文对象。然后在Wait
中设置断点,看看哪些 API 会使其被调用。
WaitOne
的标准抽吸行为实际上在多大程度上受到限制? 以下是导致 UI 线程死锁的典型示例。我在这里使用 WinForms,但同样的问题也适用于 WPF:
public partial class MainForm : Form
public MainForm()
InitializeComponent();
this.Load += (s, e) =>
Func<Task> doAsync = async () =>
await Task.Delay(2000);
;
var task = doAsync();
var handle = ((IAsyncResult)task).AsyncWaitHandle;
var startTick = Environment.TickCount;
handle.WaitOne(4000);
MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));
;
消息框将显示约 4000 毫秒的时间流逝,尽管任务只需 2000 毫秒即可完成。
发生这种情况是因为await
延续回调是通过WindowsFormsSynchronizationContext.Post
安排的,它使用Control.BeginInvoke
,而后者又使用PostMessage
,发布一条使用RegisterWindowMessage
注册的常规Windows 消息。此消息不会被发送,handle.WaitOne
会超时。
如果我们使用handle.WaitOne(Timeout.Infinite)
,我们就会遇到典型的死锁。
现在让我们实现一个带有显式泵送的WaitOne
版本(并称之为WaitOneAndPump
):
public static bool WaitOneAndPump(
this WaitHandle handle, int millisecondsTimeout)
var startTick = Environment.TickCount;
var handles = new[] handle.SafeWaitHandle.DangerousGetHandle() ;
while (true)
// wait for the handle or a message
var timeout = (uint)(Timeout.Infinite == millisecondsTimeout ?
Timeout.Infinite :
Math.Max(0, millisecondsTimeout +
startTick - Environment.TickCount));
var result = MsgWaitForMultipleObjectsEx(
1, handles,
timeout,
QS_ALLINPUT,
MWMO_INPUTAVAILABLE);
if (result == WAIT_OBJECT_0)
return true; // handle signalled
else if (result == WAIT_TIMEOUT)
return false; // timed-out
else if (result == WAIT_ABANDONED_0)
throw new AbandonedMutexException(-1, handle);
else if (result != WAIT_OBJECT_0 + 1)
throw new InvalidOperationException();
else
// a message is pending
if (timeout == 0)
return false; // timed-out
else
// do the pumping
Application.DoEvents();
// no more messages, raise Idle event
Application.RaiseIdle(EventArgs.Empty);
并像这样更改原始代码:
var startTick = Environment.TickCount;
handle.WaitOneAndPump(4000);
MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));
现在的时间间隔约为 2000 毫秒,因为 await
继续消息由 Application.DoEvents()
发送,任务完成并发出其句柄信号。
也就是说,我从不建议将 WaitOneAndPump
之类的东西用于生产代码(除了极少数特定情况)。它是 UI 重入等各种问题的根源。这些问题是 Microsoft 将标准泵送行为限制为仅适用于某些特定于 COM 的消息的原因,这对于 COM 编组至关重要。
【讨论】:
可以调整这个来解决***.com/questions/22794774/…吗? @Lijo,我愿意,我想我已经给了你link。但是,如果您没有足够的异步编程技能(顺便说一句,不涉及线程,只有异步),那将无济于事。我建议您编辑该问题并公开表示您需要在没有async/await
学习曲线的情况下完成该项目。然后我或其他人可能能够提供替代解决方案(例如单纯的回调)。
@Lijo,你可以从here复制MsgWaitForMultipleObjectsEx
。但是,你又走错了路。
@Lijo,这将是一个 ManualResetEvent
对象,您将从 DocumentCompleted
处理程序发出信号。 WaitOneAndPump
会创建一个嵌套消息循环,带来所有危险后果。
@Lijo,尽管我想接受你的赏金,但我不能推荐 WaitOneAndPump
作为最终解决方案,那是错误的。我解决这个问题的方法是this,请随意投票。无论如何,如果我能帮助到你,我很高兴。保持这个问题,以防有人发布你会更喜欢的解决方案。【参考方案2】:
实际披露了抽水的工作原理。对 .NET 运行时的内部调用又使用 CoWaitForMultipleHandles 在 STA 线程上执行等待。该 API 的文档非常缺乏,但阅读一些 COM books 和 Wine source code 可能会给您一些粗略的想法。
在内部,它使用 QS_SENDMESSAGE | 调用 MsgWaitForMultipleObjectsEx。 QS_ALLPOSTMESSAGE | QS_PAINT 标志。让我们剖析一下每一个的用途。
QS_PAINT 是最明显的,WM_PAINT 消息在消息泵中处理。 因此,在绘制处理程序中进行任何锁定是非常糟糕的主意,因为它可能会进入重入循环并导致堆栈溢出。
QS_SENDMESSAGE 用于从其他线程和应用程序发送的消息。这实际上是进程间通信工作方式的一种方式。 丑陋的部分是它还用于来自资源管理器和任务管理器的 UI 消息,因此它会泵送 WM_CLOSE 消息(右键单击任务栏中的无响应应用程序并选择关闭)、托盘图标消息和可能的东西否则 (WM_ENDSESSION)。
QS_ALLPOSTMESSAGE 用于其余部分。消息实际上是经过过滤的,因此只处理隐藏公寓窗口的消息和 DDE 消息 (WM_DDE_FIRST - WM_DDE_LAST)。
【讨论】:
【参考方案3】:我最近了解到 Process.Start 可能会抽水的困难方式。我没有等待进程,也没有询问它的 pid,我只是想让它一起运行。
在调用堆栈(我手头没有)中,我看到它进入了特定于 ShellInvoke 的代码,因此这可能仅适用于 ShellInvoke = true。
虽然整个 STA 抽水已经足够令人惊讶,但至少可以说,我发现这个非常令人惊讶!
【讨论】:
以上是关于哪些阻塞操作会导致 STA 线程泵送 COM 消息?的主要内容,如果未能解决你的问题,请参考以下文章