如何将消息发布到运行消息泵的 STA 线程?

Posted

技术标签:

【中文标题】如何将消息发布到运行消息泵的 STA 线程?【英文标题】:How to post messages to an STA thread running a message pump? 【发布时间】:2014-03-07 23:49:32 【问题描述】:

因此,在this 之后,我决定在专用 STA 线程上显式实例化 COM 对象。实验表明,COM 对象需要一个消息泵,我通过调用Application.Run() 创建:

private MyComObj _myComObj;

// Called from Main():
Thread myStaThread = new Thread(() =>

    _myComObj = new MyComObj();
    _myComObj.SomethingHappenedEvent += OnSomthingHappened;
    Application.Run();
);
myStaThread.SetApartmentState(ApartmentState.STA);
myStaThread.Start();

如何从其他线程向 STA 线程的消息泵发布消息?

注意: 为了简洁起见,我大量编辑了这个问题。 @Servy 的答案的某些部分现在似乎无关紧要,但它们是针对原始问题的。

【问题讨论】:

对于非阻塞启动不能使用 ThreadPool.QueueUserWorkerItem 吗? @Didaxis,不,因为那时消息泵没有在该线程中运行。 This answer 使用 TPL 和 async/await 实现并调用 STA 公寓。 【参考方案1】:

有没有办法启动消息泵使其不会阻塞?

没有。消息队列的要点是它需要消耗线程的执行。在实现中,消息队列看起来非常类似于您的:

while(!_stopped)

    var job = _myBlockingCollection.Take(); // <-- blocks until some job is available
    ProcessJob(job);

一个消息循环。您要做的是在同一个线程中运行两个不同的消息循环。您不能真正做到这一点(并且让两个队列都在抽水;一个队列在运行时必然会暂停另一个队列的执行),这没有任何意义。

您需要做的是向现有队列发送消息,而不是在同一线程上创建第二个消息循环。一种方法是使用SynchronizationContext。然而,一个问题是没有任何事件可以被挂钩以在消息泵中执行具有Run 重载的方法。我们需要显示一个Form,以便我们可以挂钩Shown 事件(此时我们可以隐藏它)。然后我们可以获取SynchronizationContext 并将其存储在某个地方,以便我们使用它来向消息泵发布消息:

private static SynchronizationContext context;
public static void SendMessage(Action action)

    context.Post(s => action(), null);


Form blankForm = new Form();
blankForm.Size = new Size(0, 0);
blankForm.Shown += (s, e) =>

    blankForm.Hide();
    context = SynchronizationContext.Current;
;

Application.Run(blankForm);

【讨论】:

谢谢Servy。不过,我仍然感到困惑 - 如果我理解您的解决方案,它会将操作发布到通过调用 Application.Run() 创建的消息泵。这与让 COM 为我编组调用(性能受到影响)有何不同?此外,许多地方都提到了在线程被阻塞时隐式发送 COM 消息(例如Thread.Join)。这个非阻塞消息泵是如何创建的? @bavaza Thread.Join 不会发送任何消息。它只会等到线程完成后什么都不做。 来自 MSDN:Thread.Join() - “阻塞调用线程直到线程终止,同时继续执行标准 COM 和 SendMessage 泵送。” @bavaza "这个非阻塞消息泵是如何创建的?" 不是。这就是答案的point。你不能这样做,我没有尝试。只是编写了一个有效利用阻塞消息泵的解决方案。 @bavaza 我不明白这与这里有什么关系。如果您有另一个使用它的解决方案,请务必将其发布为答案。我看不出它对你有什么帮助。【参考方案2】:

请记住,Windows 为 STA 线程创建的消息队列已经是线程安全队列的实现。因此,只需将其用于您自己的目的。这是您可以使用的基类,派生出您自己的以包含您的 COM 对象。重写 Initialize() 方法,一旦线程准备好开始执行代码,它将被调用。不要忘记在您的覆盖中调用 base.Initialize()。

如果您想在该线程上运行代码,然后使用 BeginInvoke 或 Invoke 方法,就像您使用 Control.Begin/Invoke 或 Dispatcher.Begin/Invoke 方法一样。调用它的 Dispose() 方法来关闭线程,它是可选的。请注意,只有当您 100% 确定所有 COM 对象都已完成时,才可以安全地执行此操作。由于您通常没有这种保证,因此最好不要。

using System;
using System.Threading;
using System.Windows.Forms;

class STAThread : IDisposable 
    public STAThread() 
        using (mre = new ManualResetEvent(false)) 
            thread = new Thread(() => 
                Application.Idle += Initialize;
                Application.Run();
            );
            thread.IsBackground = true;
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
            mre.WaitOne();
        
    
    public void BeginInvoke(Delegate dlg, params Object[] args) 
        if (ctx == null) throw new ObjectDisposedException("STAThread");
        ctx.Post((_) => dlg.DynamicInvoke(args), null);
    
    public object Invoke(Delegate dlg, params Object[] args) 
        if (ctx == null) throw new ObjectDisposedException("STAThread");
        object result = null;
        ctx.Send((_) => result = dlg.DynamicInvoke(args), null);
        return result;
    
    protected virtual void Initialize(object sender, EventArgs e) 
        ctx = SynchronizationContext.Current;
        mre.Set();
        Application.Idle -= Initialize;
    
    public void Dispose() 
        if (ctx != null) 
            ctx.Send((_) => Application.ExitThread(), null);
            ctx = null;
        
    
    private Thread thread;
    private SynchronizationContext ctx;
    private ManualResetEvent mre;

【讨论】:

Here 你提到让 COM 进行编组通常比直接调用消息本身慢 10000 倍左右。您发布的解决方案会比让 COM 进行编组更快吗?如果是这样,为什么 COM 需要更长的时间才能基本上发布到消息队列? 您创建了一个 STA 线程,因此对 COM 对象的方法调用不必进行编组。不知道 BlockingQueue 到底是什么,但如果你将它换成 Begin/Invoke 调用,那么不会,这不太可能有太大的不同。至少你可以一次调用执行一堆代码,并且可以避免被千针扎死。 @HansPassant:您是否发现在我的应用程序中创建此类的多个实例有任何问题? @HansPassant 我正在尝试使用它,但在构造函数中完全正常的 mre 对象在 Initialze 函数中为空。怎么回事? 如果有人想知道 (_) 在 lambda 表达式中的含义,它是一种 "Don't Care" parameter name. SendOrPostCallback delegate takes in an object as a parameter 但此代码的 lambda 表达式不需要任何参数。

以上是关于如何将消息发布到运行消息泵的 STA 线程?的主要内容,如果未能解决你的问题,请参考以下文章

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

在 ASP.NET 中将线程设置为 STA

Akka 如何决定哪个 Actor 获得线程?

如何并行运行 Qt GUI 和 Linux 消息队列接收线程?

如何在 tkinter 的线程中运行的 websocket 下发送消息?

如何将消息/数据从广播接收器(process1)发送到android中的服务线程(process2)?