Подтвердить что ты не робот

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

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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.

Ответ 2

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

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

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

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

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

Ответ 3

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

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

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