How to post messages to an STA thread running a message pump?
Asked Answered
D

2

17

So, following this, I decided to explicitly instantiate a COM object on a dedicated STA thread. Experiments showed that the COM object needed a message pump, which I created by calling 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();

How do I post messages the the STA thread's message pump from other threads?

Note: I heavily edited the question for the sake of brevity. Some parts of @Servy's answer now seems unrelated, but they were for the original question.

Dunlin answered 10/2, 2014 at 15:9 Comment(3)
For a non-blocking initiation can't you use ThreadPool.QueueUserWorkerItem?Ran
@Didaxis, no, because then the message pump isn't running in that thread.Roley
This answer uses TPL and async/await to implement and call into an STA apartment.Verrazano
A
27

Keep in mind that the message queue that Windows creates for an STA thread is already an implementation of a thread-safe queue. So just use it for your own purposes. Here's a base class that you can use, derive your own to include your COM object. Override the Initialize() method, it will be called as soon as the thread is ready to start executing code. Don't forget to call base.Initialize() in your override.

It you want to run code on that thread then use the BeginInvoke or Invoke methods, just like you would for the Control.Begin/Invoke or Dispatcher.Begin/Invoke methods. Call its Dispose() method to shut down the thread, it is optional. Beware that this is only safe to do when you are 100% sure that all COM objects are finalized. Since you don't usually have that guarantee, it is better that you don't.

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;
}
Acariasis answered 10/2, 2014 at 17:32 Comment(5)
Here you mentioned that letting COM do the marshaling is typically around 10000 times slower than a direct call on the message itself. Will the solution you posted be faster then letting COM do the marshaling? If so, how come COM takes longer to basically post to the message queue?Dunlin
You created an STA thread so the method calls on the COM object don't have to be marshaled. No idea what the BlockingQueue was all about, but if you traded that for Begin/Invoke calls then no, that's unlikely to make much of a difference. At least you can execute a bunch of code with a single invoke and can avoid death by a thousand needle pricks.Acariasis
@HansPassant: Do you see any issues with creating multiple instances of this class in my application?Titan
@HansPassant I am trying to use this but the mre object which is perfectly fine in the constructor is null in the Initialze function. What's going on?Aguiar
In case anyone's wondering what the (_) means in the lambda expression, it's a sort of "Don't Care" parameter name. The SendOrPostCallback delegate takes in an object as a parameter but this code's lambda expression doesn't need any parameters.Lashing
R
4

Is there a way to start the message pump so it does not block?

No. The point of a message queue is that it needs to consume the thread's execution. A message queue is, in implementation, going to look very similar to your:

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

That is a message loop. What you're trying to do is run two different message loops in the same thread. You can't really do that (and have both queues pumping; one queue will, by necessity, pause execution of the other while it is running), it just doesn't make sense.

What you need to do, instead of creating a second message loop on the same thread, is send messages to your existing queue. One way of doing that is through the use of a SynchronizationContext. One problem however is that there aren't any events that can be hooked into to execute a method in the message pump with that overload of Run. We'll need to show a Form just so that we can hook into the Shown event (at which point we can hide it). We can then grab the SynchronizationContext and store it somewhere, allowing us to use it to post messages to the message pump:

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);
Roley answered 10/2, 2014 at 15:43 Comment(6)
Thank you Servy. I am still baffled though - if I understand your solution, it is posting actions to the message pump created by calling Application.Run(). How is that different from letting COM marshal the calls for me (with performance hit)? Moreover, many places mention implicit pumping of COM messages while the thread is blocked (e.g. Thread.Join). How was this non-blocking message pump created?Dunlin
@Dunlin Thread.Join isn't going to be pumping any messages. It's just going to do nothing but wait until the thread finishes.Roley
from MSDN: Thread.Join() - "Blocks the calling thread until a thread terminates, while continuing to perform standard COM and SendMessage pumping."Dunlin
@Dunlin " How was this non-blocking message pump created? " It wasn't. That was the point of the answer. You cannot do this, and I didn't try. Is simply wrote a solution that effectively utilizes a blocking message pump.Roley
@Dunlin I don't see how that's relevant here anyway. If you have another solution that uses that, by all means post it as an answer. I don't see how it helps you here.Roley
Fair enough. Perhaps I should rephrase the question to: "How to post messages to an STA thread running a message pump"Dunlin

© 2022 - 2024 — McMap. All rights reserved.