哪些阻塞操作会导致 STA 线程泵送 COM 消息?

Which blocking operations cause an STA thread to pump COM messages?(哪些阻塞操作会导致 STA 线程泵送 COM 消息?)

本文介绍了哪些阻塞操作会导致 STA 线程泵送 COM 消息?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当在 STA 线程上实例化 COM 对象时,该线程通常必须实现消息泵,以便编组对其他线程的调用(请参阅 这里).

When a COM object is instantiated on an STA thread, the thread usually has to implement a message pump in order to marshal calls to and fro other threads (see here).

可以手动泵送消息,也可以依靠某些但不是全部线程阻塞操作会在等待时自动泵送与 COM 相关的消息这一事实.文档通常无助于确定哪个是哪个(请参阅此相关问题).

One can either pump messages manually, or rely on the fact that some, but not all, thread-blocking operations will automatically pump COM-related messages while waiting. The documentation often doesn't help in deciding which is which (see this related question).

如何确定线程阻塞操作是否会在 STA 上泵送 COM 消息?

How can I determine if a thread-blocking operation will pump COM messages on an STA?

到目前为止的部分列表:

Partial lists so far:

阻止进行抽水的操作*:

  • Thread.Join
  • WaitHandle.WaitOne/WaitAny/WaitAll(WaitAll 不能从 STA 线程调用)
  • GC.WaitForPendingFinalizers
  • Monitor.Enter(因此 lock)- 在某些情况下
  • ReaderWriterLock
  • BlockingCollection
  • Thread.Join
  • WaitHandle.WaitOne/WaitAny/WaitAll (WaitAll cannot be called from an STA thread though)
  • GC.WaitForPendingFinalizers
  • Monitor.Enter (and therefore lock) - under some conditions
  • ReaderWriterLock
  • BlockingCollection

阻止抽水的操作:

  • Thread.Sleep
  • Console.ReadKey(在某处阅读)
  • Thread.Sleep
  • Console.ReadKey (read it somewhere)

*注意 Noseratio 的回答 说即使是抽水操作,也只能针对非常有限的未公开的 COM-具体消息.

*Note Noseratio's answer saying that even operations which do pump, do so for a very limited undisclosed set of COM-specific messages.

推荐答案

BlockingCollection 确实会在阻塞时抽水.我在回答以下问题时了解到这一点,其中有一些关于 STA 泵送的有趣细节:

BlockingCollection will indeed pump while blocking. I've learnt that while answering the following question, which has some interesting details about STA pumping:

StaTaskScheduler 和 STA 线程消息泵送

但是,它将发送一组非常有限的未公开的 COM 特定消息,与您列出的其他 API 相同.它不会发送通用 Win32 消息(一个特殊情况是 WM_TIMER,也不会发送).这可能是一些需要全功能消息循环的 STA COM 对象的问题.

However, it will pump a very limited undisclosed set of COM-specific messages, same as the other APIs you listed. It won't pump general purpose Win32 messages (a special case is WM_TIMER, which won't be dispatched either). This might be a problem for some STA COM objects which expect a full-featured message loop.

如果您想对此进行试验,请创建自己的 SynchronizationContext 版本,覆盖 SynchronizationContext.Wait,调用SetWaitNotificationRequired 并安装您的自定义STA 线程上的同步上下文对象.然后在 Wait 中设置断点,看看哪些 API 会使其被调用.

If you like to experiment with this, create your own version of SynchronizationContext, override SynchronizationContext.Wait, call SetWaitNotificationRequired and install your custom synchronization context object on an STA thread. Then set a breakpoint inside Wait and see what APIs will make it get called.

WaitOne 的标准抽水行为实际上在多大程度上受到限制? 下面是一个导致 UI 线程死锁的典型示例.我在这里使用 WinForms,但同样的问题也适用于 WPF:

To what extent the standard pumping behavior of WaitOne is actually limited? Below is a typical example causing a deadlock on the UI thread. I use WinForms here, but the same concern applies to 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 毫秒即可完成.

The message box will show the time lapse of ~ 4000 ms, although the task takes only 2000 ms to complete.

发生这种情况是因为 await 延续回调是通过 WindowsFormsSynchronizationContext.Post 安排的,它使用 Control.BeginInvoke,而 Control.BeginInvoke 又使用 PostMessage,发布使用 RegisterWindowMessage 注册的常规 Windows 消息.此消息不会被发送并且 handle.WaitOne 超时.

That happens because the await continuation callback is scheduled via WindowsFormsSynchronizationContext.Post, which uses Control.BeginInvoke, which in turn uses PostMessage, posting a regular Windows message registered with RegisterWindowMessage. This message doesn't get pumped and handle.WaitOne times out.

如果我们使用 handle.WaitOne(Timeout.Infinite),我们就会遇到典型的死锁.

If we used handle.WaitOne(Timeout.Infinite), we'd have a classic deadlock.

现在让我们实现一个带有显式泵送的 WaitOne 版本(并将其称为 WaitOneAndPump):

Now let's implement a version of WaitOne with explicit pumping (and call it 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);
            }
        }
    }
}

并像这样更改原始代码:

And change the original code like this:

var startTick = Environment.TickCount;
handle.WaitOneAndPump(4000);
MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));

现在的时间间隔约为 2000 毫秒,因为 await 继续消息由 Application.DoEvents() 泵送,任务完成并发出其句柄信号.

The time lapse now will be ~2000 ms, because the await continuation message gets pumped by Application.DoEvents(), the task completes and its handle is signaled.

也就是说,我从不建议将 WaitOneAndPump 之类的东西用于生产代码(除了极少数特定情况).它是 UI 重入等各种问题的根源.这些问题是微软将标准泵送行为仅限于某些特定于 COM 的消息的原因,这对于 COM 编组至关重要.

That said, I'd never recommend using something like WaitOneAndPump for production code (besides for very few specific cases). It's a source of various problems like UI re-entrancy. Those problems are the reason Microsoft has limited the standard pumping behavior to only certain COM-specific messages, vital for COM marshaling.

这篇关于哪些阻塞操作会导致 STA 线程泵送 COM 消息?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!

本文标题为:哪些阻塞操作会导致 STA 线程泵送 COM 消息?

基础教程推荐