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

16

所以,根据这个,我决定在专用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
@Didaxis,不行,因为这样消息泵就无法在该线程中运行。 - Servy
这个答案使用TPL和async/await来实现并调用STA单元。 - noseratio - open to work
2个回答

26

请记住,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;
}

在这里,您提到让COM进行编组通常比直接调用消息本身慢大约10000倍。您发布的解决方案是否比让COM进行编组更快?如果是这样,那么为什么COM花费更长的时间基本上是发布到消息队列? - bavaza
1
你创建了一个STA线程,所以不需要对COM对象上的方法调用进行编组。虽然不知道BlockingQueue是什么,但如果你将其替换为Begin/Invoke调用,那么不会有太大的区别。至少你可以使用单个调用执行大量代码,并避免被千针刺死。 - Hans Passant
@HansPassant:你认为在我的应用程序中创建多个该类的实例会有什么问题吗? - dotNET
@HansPassant 我正在尝试使用这个,但是在Initialize函数中mre对象在构造函数中完全正常却为null。发生了什么? - Dale M
1
如果有人想知道lambda表达式中的(_)是什么意思,它是一种“不关心”的参数名称。SendOrPostCallback委托以对象作为参数,但这段代码的lambda表达式不需要任何参数。 - jrh

4

有没有方法可以启动消息泵而不阻塞?

没有。消息队列的目的是需要消耗线程的执行。实现上,消息队列看起来会非常类似于你的:

while(!_stopped)
{
    var job = _myBlockingCollection.Take(); // <-- blocks until some job is available
    ProcessJob(job);
}

那是一个消息循环。你试图在同一线程中运行两个不同的消息循环,但这并不现实(且只有一个队列能够运作;当其中一个队列正在运行时,为了必要性另一个队列将会暂停执行其他操作)。这种做法没有意义。
相反地,你需要通过向现有队列发送消息来解决问题。其中一种方法是通过使用“同步上下文”(SynchronizationContext)。但一个问题是,在那种重载版本的Run方法中,没有可以钩入以执行消息泵中的方法的事件。我们需要显示一个窗体以便我们能够钩入Shown事件(在此时隐藏)。然后,我们可以获取同步上下文并将其存储在某处,使我们能够将其用于将消息发布到消息泵:
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
@bavaza Thread.Join 不会传递任何消息。它只会等待线程完成,什么也不做。 - Servy
Thread.Join() - "阻塞调用线程,直到一个线程终止,同时继续执行标准的COM和SendMessage消息泵。" - bavaza
@bavaza "这个非阻塞消息泵是如何创建的?" 实际上没有被创建。这就是答案的重点。你无法做到这一点,我也没有尝试。我只是编写了一个有效利用阻塞消息泵的解决方案。 - Servy
@bavaza 我不明白那与此有何关系。如果你有另一种使用它的解决方案,请尽管将其发布为答案。我不明白它如何对你有所帮助。 - Servy
好的,也许我应该重新表述问题:“如何向运行消息泵的STA线程发布消息” - bavaza

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接