Как публиковать сообщения в ветке 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 теперь кажутся не связанными, но они были для оригинального вопроса.

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

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

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

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);
Другие вопросы по тегам