Какие операции блокировки заставляют поток STA перекачивать сообщения COM?

Когда COM-объект создается в потоке STA, поток обычно должен реализовать насос сообщений, чтобы перенаправлять вызовы в другие потоки (см. Здесь).

Можно либо перекачивать сообщения вручную, либо полагаться на то, что некоторые, но не все, операции блокировки потоков будут автоматически перекачивать сообщения, связанные с COM, во время ожидания. Документация часто не помогает решить, какой именно (см. Этот связанный вопрос).

Как я могу определить, будет ли операция блокировки потока перекачивать сообщения COM на STA?

Частичные списки пока:

Операции блокировки, которые делают насос *:

  • Thread.Join
  • WaitHandle.WaitOne / WaitAny / WaitAll (WaitAll не может быть вызван из потока STA, хотя)
  • GC.WaitForPendingFinalizers
  • Monitor.Enter (и поэтому lock) - при некоторых условиях
  • ReaderWriterLock
  • BlockingCollection

Операции блокировки, которые не прокачивают:

  • Thread.Sleep
  • Console.ReadKey (прочитайте это где-нибудь)

* Обратите внимание на ответ Носерацио, в котором говорится, что даже операции, которые выполняют насос, делают это для очень ограниченного нераскрытого набора специфических для COM сообщений.

3 ответа

Решение

BlockingCollection будет действительно качать при блокировке. Я узнал об этом, отвечая на следующий вопрос, в котором есть некоторые интересные детали о прокачке STA:

StaTaskScheduler и прокачка сообщений потока STA

Однако он прокачает очень ограниченный нераскрытый набор сообщений, специфичных для COM, так же, как и другие API, которые вы перечислили. Это не будет качать сообщения общего назначения Win32 (особый случай WM_TIMER, который также не будет отправлен). Это может быть проблемой для некоторых объектов STA COM, которые ожидают полнофункциональный цикл обработки сообщений.

Если вам нравится экспериментировать с этим, создайте свою собственную версию SynchronizationContext переопределить SynchronizationContext.Wait, вызов SetWaitNotificationRequired и установите пользовательский объект контекста синхронизации в потоке STA. Затем установите точку останова внутри Wait и посмотрите, какие API будут вызывать его.

В какой степени стандартное поведение накачки WaitOne на самом деле ограничен? Ниже приведен типичный пример, вызывающий тупик в потоке пользовательского интерфейса. Я использую WinForms здесь, но то же самое относится и к WPF:

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();

        this.Load += (s, e) =>
        {
            Func<Task> doAsync = async () =>
            {
                await Task.Delay(2000);
            };

            var task = doAsync();
            var handle = ((IAsyncResult)task).AsyncWaitHandle;

            var startTick = Environment.TickCount;
            handle.WaitOne(4000);
            MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));
        };
    }
}

Окно сообщения покажет промежуток времени ~ 4000 мс, хотя выполнение задачи занимает всего 2000 мс.

Это происходит потому, что await продолжение обратного вызова запланировано через WindowsFormsSynchronizationContext.Post, который использует Control.BeginInvoke который в свою очередь использует PostMessage, публикуя обычное сообщение Windows, зарегистрированное в RegisterWindowMessage, Это сообщение не перекачивается и handle.WaitOne время вышло.

Если бы мы использовали handle.WaitOne(Timeout.Infinite) У нас был бы классический тупик.

Теперь давайте реализуем версию WaitOne с явной накачкой (и назовите это WaitOneAndPump):

public static bool WaitOneAndPump(
    this WaitHandle handle, int millisecondsTimeout)
{
    var startTick = Environment.TickCount;
    var handles = new[] { handle.SafeWaitHandle.DangerousGetHandle() };

    while (true)
    {
        // wait for the handle or a message
        var timeout = (uint)(Timeout.Infinite == millisecondsTimeout ?
                Timeout.Infinite :
                Math.Max(0, millisecondsTimeout + 
                    startTick - Environment.TickCount));

        var result = MsgWaitForMultipleObjectsEx(
            1, handles,
            timeout,
            QS_ALLINPUT,
            MWMO_INPUTAVAILABLE);

        if (result == WAIT_OBJECT_0)
            return true; // handle signalled
        else if (result == WAIT_TIMEOUT)
            return false; // timed-out
        else if (result == WAIT_ABANDONED_0)
            throw new AbandonedMutexException(-1, handle);
        else if (result != WAIT_OBJECT_0 + 1)
            throw new InvalidOperationException();
        else
        {
            // a message is pending 
            if (timeout == 0)
                return false; // timed-out
            else
            {
                // do the pumping
                Application.DoEvents();
                // no more messages, raise Idle event
                Application.RaiseIdle(EventArgs.Empty);
            }
        }
    }
}

И измените исходный код следующим образом:

var startTick = Environment.TickCount;
handle.WaitOneAndPump(4000);
MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));

Промежуток времени теперь будет ~2000 мс, потому что await сообщение продолжения перекачивается Application.DoEvents(), задача завершается, и ее дескриптор сигнализируется.

Тем не менее, я никогда не рекомендую использовать что-то вроде WaitOneAndPump для производственного кода (кроме очень немногих конкретных случаев). Это источник различных проблем, таких как повторный вход пользовательского интерфейса. Эти проблемы являются причиной того, что Microsoft ограничила стандартное поведение накачки только определенными COM-специфическими сообщениями, жизненно важными для COM-маршалинга.

Как работает насос на самом деле раскрыто. Существуют внутренние вызовы среды выполнения.NET, которые, в свою очередь, используют CoWaitForMultipleHandles для выполнения ожидания в потоках STA. Документации для этого API довольно мало, но чтение некоторых книг по COM и исходного кода Wine может дать вам некоторые грубые идеи.

Внутренне он вызывает MsgWaitForMultipleObjectsEx с QS_SENDMESSAGE | QS_ALLPOSTMESSAGE | QS_PAINT flags. Давайте рассмотрим, для чего используется каждый.

QS_PAINT является наиболее очевидным, сообщения WM_PAINT обрабатываются в насосе сообщений. Таким образом, это действительно плохая идея - делать какие-либо блокировки в обработчиках рисования, потому что они, вероятно, попадут в повторяющийся цикл и вызовут переполнение стека.

QS_SENDMESSAGE для сообщений, отправленных из других потоков и приложений. Это на самом деле один из способов работы межпроцессного взаимодействия. Уродливая часть заключается в том, что он также используется для сообщений пользовательского интерфейса из Проводника и Диспетчера задач, поэтому он перекачивает сообщение WM_CLOSE (щелкните правой кнопкой мыши на неотвечающем приложении на панели задач и выберите "Закрыть"), сообщения в трее и, возможно, что-то еще (WM_ENDSESSION).).

QS_ALLPOSTMESSAGE для остальных. Сообщения фактически фильтруются, поэтому обрабатываются только сообщения для скрытого окна квартиры и сообщения DDE (WM_DDE_FIRST - WM_DDE_LAST).

Недавно я узнал о том, как сильно может работать Process.Start. Я не ждал процесса и не спрашивал его pid, я просто хотел, чтобы он шел рядом.

В стеках вызовов (у меня нет под рукой) я видел, что это входит в специфичный для ShellInvoke код, так что это может относиться только к ShellInvoke = true.

В то время как вся прокачка STA достаточно удивительна, я обнаружил, что это очень удивительно, если не сказать больше!

Другие вопросы по тегам