16

Вопрос по c#, com, interop, message-pump – Какие операции блокировки заставляют поток STA перекачивать сообщения COM?

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

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

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

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

Блокирующие операции, которыеделать насос *:

Thread.JoinWaitHandle.WaitOne/WaitAny/WaitAll (WaitAll не может быть вызван из потока STA, хотя)GC.WaitForPendingFinalizersMonitor.Enter (и поэтомуlock) - при некоторых условияхReaderWriterLockBlockingCollection

Блокирующие операции, которыене делайте насос:

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

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

Вы можете посмотреть справочный источник или использовать декомпилятор. На самом деле это не имеет значения, все ожидания объекта синхронизации в конечном итоге используют один и тот же код внутри CLR.

от Hans Passant

@HansPassant - как можно определить, какой тип объекта sych используется классом-оберткой? Например, документы не указывают, какBlockingCollection.Take() на самом деле блоки.

от bavaza

Классы, которые обертывают невидимый объект синхронизации, например BlockingCollection, попадают в скобку WaitHandle.Wait.

от Hans Passant

@SimonMourier - спасибо за исправление - я обновлю вопрос.

от bavaza

Утверждение «Как правило, объект COM должен быть создан на STA» неверно. Здесь нет «общности» и «нет необходимости», так как это действительно зависит от объекта COM, от того, как он объявлен в COM. В самом деле, COM делает всю работу за вас, чтобы избежать этих «необходимости» (иногда ценой нежелательного маршалинга в полном объеме).

от Simon Mourier
3 ответа
5

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-маршалинга.

@ Лайо, я думаю, я дал тебессылка на сайт уже. Но это не помогло бы, если бы у вас не было достаточно навыков для асинхронного программирования (между прочим, не было никакого потока, только асинхронность). Я предлагаю вам отредактировать этот вопрос и открыто сказать, что вам нужно сделать этот проект ASP безasync/await кривая обучения. Тогда я или кто-то еще сможем предложить альтернативное решение (например, простые обратные вызовы).

от noseratio

@Lijo, это было быManualResetEvent объект, который вы бы сигнал отDocumentCompleted обработчик.WaitOneAndPump создаст вложенный цикл сообщений со всеми опасными последствиями.

от noseratio

@Lijo, сколько бы я ни хотел взять за награду, я не могу рекомендоватьWaitOneAndPump как окончательное решение, это было бы неправильно. Мой подход к этой проблемеэтотне стесняйтесь голосовать вместо этого. Во всяком случае, я рад, если я помог вам. Держите этот вопрос подальше, на тот случай, если кто-то опубликует решение, которое вам понравится больше.

от noseratio

@Lijo, вы можете скопироватьMsgWaitForMultipleObjectsEx отВот, Но опять же, вы идете по неверному пути.

от noseratio
2

Недавно я узнал

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

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

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

1

Как работает насос на самом деле раскрыто. Существуют внутренние вызов

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

Внутренне он вызывает 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).

Похожие вопросы