Как отправлять сообщения в поток STA с помощью насоса сообщений?

Итак, после этого, я решил явно создать экземпляр COM-объекта в выделенном потоке STA. Эксперименты показали, что 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 теперь кажутся несвязанными, но они были для оригинального вопроса.

Ответ 1

Имейте в виду, что очередь сообщений, созданная 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;
}

Ответ 2

Есть ли способ запустить насос сообщений, чтобы он не блокировался?

Нет. Точка очереди сообщений заключается в том, что она должна потреблять выполнение потока. Очередь сообщений в процессе реализации выглядит очень похожей на вашу:

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