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

bav*_*aza 17 c# com interop message-pump

当一个COM对象在STA线程上实例化时,该线程通常必须实现一个消息泵,以便为来回调用其他线程(见这里).

可以手动泵送消息,或者依赖于某些(但不是全部)线程阻塞操作在等待时自动泵送COM相关消息的事实.文档通常无助于决定哪个是哪个(参见相关问题).

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

到目前为止的部分清单:

阻断其业务泵*:

阻止泵送的操作:

*注意Noseratio的答案说,即使是操作泵,也是为非常有限的未公开的COM特定消息集.

nos*_*tio 5

BlockingCollection确实会阻塞.我已经了解到,在回答以下问题时,其中有一些有关STA抽吸的有趣细节:

StaTaskScheduler和STA线程消息泵送

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

如果您想试验这一点,请在STA线程上创建自己的版本SynchronizationContext,覆盖SynchronizationContext.Wait,调用SetWaitNotificationRequired和安装自定义同步上下文对象.然后在里面设置一个断点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));
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

消息框将显示~4000 ms的时间间隔,但任务只需2000 ms即可完成.

之所以发生这种情况,是因为await继续回调是通过调度来进行的WindowsFormsSynchronizationContext.Post,后者使用Control.BeginInvoke,然后使用PostMessage,发布注册的常规Windows消息RegisterWindowMessage.此消息不会被抽出并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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

并像这样更改原始代码:

var startTick = Environment.TickCount;
handle.WaitOneAndPump(4000);
MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));
Run Code Online (Sandbox Code Playgroud)

现在时间流逝将是~2000毫秒,因为await连续消息被抽出Application.DoEvents(),任务完成并且其信号被发出信号.

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

  • @Lijo,我知道,我想我已经给你了[link](http://stackoverflow.com/a/22262976/1768303).但是如果你没有足够的异步编程技能(没有涉及btw的线程,只有异步),那就无济于事了.我建议你编辑这个问题并公开表示你需要在没有`async/await`学习曲线的情况下完成ASP项目.然后我或其他人可能能够提供替代解决方案(例如仅仅回调). (2认同)