18

当在 STA 线程上实例化 COM 对象时,该线程通常必须实现消息泵,以便编组对其他线程的调用(参见此处)。

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

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

到目前为止的部分列表:

阻塞泵*的操作:

阻塞泵送的操作:

*注意Noseratio 的回答说,即使是抽水操作,也是为了一组非常有限的未公开的 COM 特定消息。

4

3 回答 3

6

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));
        };
    }
}

消息框将显示约 4000 毫秒的时间流逝,尽管该任务只需 2000 毫秒即可完成。

发生这种情况是因为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);
            }
        }
    }
}

并像这样更改原始代码:

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

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

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

于 2014-02-05T09:46:24.783 回答
3

泵的工作原理实际上已被披露。对 .NET 运行时的内部调用又使用 CoWaitForMultipleHandles 在 STA 线程上执行等待。该 API 的文档非常缺乏,但阅读一些COM 书籍Wine 源代码可以为您提供一些粗略的想法。

在内部,它使用 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)。

于 2017-08-24T21:25:07.790 回答
2

我最近了解到 Process.Start 可能会抽水的艰难方式。我没有等待进程,也没有询问它的 pid,我只是希望它与它一起运行。

在调用堆栈(我手头没有)中,我看到它进入了特定于 ShellInvoke 的代码,因此这可能仅适用于 ShellInvoke = true。

虽然整个 STA 抽水已经足够令人惊讶了,但我发现这一次非常令人惊讶,至少可以这么说!

于 2017-11-11T20:03:24.293 回答