.NET: как вызвать делегата в определенном потоке? (ISynchronizeInvoke, Dispatcher, AsyncOperation, SynchronizationContext и т. Д.)

Прежде всего обратите внимание, что этот вопрос не помечен тегом winforms или wpf или любым другим, специфичным для графического интерфейса пользователя . Как вы вскоре увидите, это сделано намеренно.

Во-вторых, извините, если вопрос слишком длинный. Я пытаюсь собрать воедино различные фрагменты информации, плавающие здесь и там, чтобы также предоставить ценную информацию. Однако мой вопрос находится прямо в разделе «Что я хотел бы знать».

Моя миссия - наконец понять различные способы, предлагаемые .NET для вызова делегата в конкретном потоке.


Что хотелось бы знать:

  • Я ищу наиболее общий способ (не зависящий от Winforms или WPF) для вызова делегатов в определенных потоках.

  • Или, иначе говоря: мне было бы интересно, используют ли и как различные способы сделать это (например, через Dispatcher WPF) друг друга; то есть, если есть один общий механизм для вызова делегата между потоками, который используется всеми остальными.


Что я уже знаю:

  • Есть много классов, связанных с этой темой; из их:

    • SynchronizationContext (в _3 _)
      Если бы я угадал, это был бы самый простой вопрос; хотя я не понимаю, что именно он делает и как его используют.

    • AsyncOperation & _ 5_ System.ComponentModel)
      Кажется, быть оберткой вокруг SynchronizationContext. Не знаю, как их использовать.

    • WindowsFormsSynchronizationContext System.Windows.Forms)
      Подкласс SynchronizationContext.

    • ISynchronizeInvoke (в _12 _)
      Используется Windows Forms. (Класс Control реализует это. Если бы мне пришлось угадывать, я бы сказал, что эта реализация использует WindowsFormsSynchronizationContext.)

    • Dispatcher & DispatcherSynchronizationContext < em> (in System.Windows.Threading)
      Похоже, что последний является еще одним подклассом SynchronizationContext, и первый делегирует ему.

  • Некоторые потоки имеют собственный цикл сообщений вместе с очередью сообщений.

    (Страница MSDN О сообщениях и очередях сообщений содержит вводную справочную информацию о том, как циклы сообщений работают на системном уровне, т. е. очереди сообщений как Windows API.)

    Я вижу, как можно реализовать вызов между потоками для потоков с очередью сообщений. Используя Windows API, вы можете поместить сообщение в очередь сообщений определенного потока через _ 19_, который содержит инструкцию для вызова некоторого делегата. Цикл сообщений, который выполняется в этом потоке, в конечном итоге дойдет до этого сообщения, и будет вызван делегат.

    Из того, что я прочитал в MSDN, поток автоматически не имеет собственной очереди сообщений. Очередь сообщений станет доступной, например. когда поток создал окно. Без очереди сообщений для потока не имеет смысла иметь цикл сообщений.

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


person stakx - no longer contributing    schedule 30.01.2011    source источник


Ответы (2)


Приносим извинения за такой длинный ответ. Но я подумал, что стоит объяснить, что именно происходит.

Ага! Думаю, я понял это. Наиболее общий способ вызова делегата в конкретном потоке действительно кажется классом SynchronizationContext.

Во-первых, платформа .NET не предоставляет средства по умолчанию для простой «отправки» делегата в любой поток, чтобы он немедленно выполнялся там. Очевидно, это не может сработать, потому что это означало бы «прерывание» любой работы, которую поток выполнял бы в то время. Следовательно, целевой поток сам решает, как и когда он будет «получать» делегатов; то есть эта функциональность должна предоставляться программистом.

Таким образом, целевой поток нуждается в каком-то способе «приема» делегатов. Это можно сделать разными способами. Один простой механизм состоит в том, чтобы поток всегда возвращался в некоторый цикл (назовем его «цикл сообщений»), где он будет смотреть на очередь. Он сработает все, что находится в очереди. Windows изначально работает так, когда дело доходит до вещей, связанных с пользовательским интерфейсом.

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


Пример:

Шаг 1. Давайте сначала создадим SynchronizationContext класс, который будет использоваться вместе с очередью сообщений целевого потока:

class QueueSyncContext : SynchronizationContext
{
    private readonly ConcurrentQueue<SendOrPostCallback> queue;

    public QueueSyncContext(ConcurrentQueue<SendOrPostCallback> queue)
    {
        this.queue = queue;
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        queue.Enqueue(d);
    }

    // implementation for Send() omitted in this example for simplicity's sake.
}

По сути, это не более чем добавление всех делегатов, переданных через Post, в очередь, предоставленную пользователем. (Post - это метод для асинхронных вызовов. Send предназначен для синхронных вызовов. Я пока опускаю последний.)

Шаг 2. Теперь напишем код для потока Z, который ожидает прибытия делегатов d:

SynchronizationContext syncContextForThreadZ = null;

void MainMethodOfThreadZ()
{
    // this will be used as the thread's message queue:
    var queue = new ConcurrentQueue<PostOrCallDelegate>();

    // set up a synchronization context for our message processing:
    syncContextForThreadZ = new QueueSyncContext(queue);
    SynchronizationContext.SetSynchronizationContext(syncContextForThreadZ);

    // here's the message loop (not efficient, this is for demo purposes only:)
    while (true)
    {
        PostOrCallDelegate d = null;
        if (queue.TryDequeue(out d))
        {
            d.Invoke(null);
        }
    }
}

Шаг 3. Поток Z нужно где-то запускать:

new Thread(new ThreadStart(MainMethodOfThreadZ)).Start();

Шаг 4. Наконец, вернемся к другой теме A, мы хотим отправить делегата в тему Z:

void SomeMethodOnThreadA()
{
    // thread Z must be up and running before we can send delegates to it:
    while (syncContextForThreadZ == null) ;

    syncContextForThreadZ.Post(_ =>
        {
            Console.WriteLine("This will run on thread Z!");
        },
        null);
}

Приятно то, что SynchronizationContext работает независимо от того, работаете ли вы в приложении Windows Forms, в приложении WPF или в многопоточном консольном приложении, созданном вами. И Winforms, и WPF предоставляют и устанавливают подходящие SynchronizationContexts для своего основного потока / потока пользовательского интерфейса.

Общая процедура вызова делегата в конкретном потоке следующая:

  • Вы должны захватить (Z) SynchronizationContext целевого потока, чтобы вы могли Send (синхронно) или Post (асинхронно) делегатом этому потоку. Для этого нужно сохранить контекст синхронизации, возвращаемый SynchronizationContext.Current, пока вы находитесь в целевом потоке Z. (Этот контекст синхронизации должен быть ранее зарегистрирован в / потоком Z.) Затем сохраните эту ссылку где-нибудь, где она доступна для потока A.

  • Находясь в потоке A, вы можете использовать захваченный контекст синхронизации для отправки или публикации любого делегата в потоке Z: zSyncContext.Post(_ => { ... }, null);

person stakx - no longer contributing    schedule 30.01.2011
comment
Реализация QueueSyncContext.Send неверна, потому что она вызывает делегат в потоке вызывающего. Цель Send - заблокировать вызывающего, пока делегат работает в потоке, представленном SynchronizationContext. Это гарантирует, что делегат получает доступ к нужным локальным объектам потока, не запускается одновременно с другими делегатами, вызываемыми через Send или Post (во избежание состояния гонки) и т. Д. (Обратите внимание, что это не относится к свободным потокам. контексты синхронизации, которые, например, запускают делегаты в пуле потоков, но я не думал, что это было вашим намерением здесь.) - person Bradley Grainger; 30.01.2011
comment
@Bradley: Вы правы, и я удалил эту реализацию из ответа именно по этой причине, пока вы ее комментировали. Было бы слишком далеко, если бы простой пример кода позаботился о необходимой синхронизации. - person stakx - no longer contributing; 30.01.2011
comment
Хорошая рецензия. Прежде чем я обнаружил этот пост, я искал ответ на этот вопрос и нашел пример в IDesign; см. элемент в разделе «Загрузки .NET» под названием «Пользовательский контекст синхронизации». Кстати, гл. 16 статьи Джо Даффи «Параллельное программирование в Windows» очень подробно описывает каждый из классов BCL, которые вы перечисляете в своем исходном посте. - person Andrew Brown; 21.03.2011
comment
@Andrew, спасибо и спасибо за ссылку на эту книгу. Кстати, есть также неплохая статья в Code Project о SynchronizationContext. - person stakx - no longer contributing; 21.03.2011

Если вы хотите поддержать вызов делегата в потоке, который иначе не имеет цикла сообщений, вам, по сути, необходимо реализовать свой собственный.

В цикле сообщений нет ничего особенного волшебства: это как потребитель в обычном шаблоне производитель / потребитель. Он хранит очередь дел, которые нужно сделать (обычно события, на которые нужно реагировать), и проходит через очередь, действуя соответствующим образом. Когда больше нечего делать, он ждет, пока что-то не будет помещено в очередь.

Другими словами: вы можете представить поток с циклом сообщений как однопоточный пул потоков.

Вы можете достаточно легко реализовать это самостоятельно, в том числе в консольном приложении. Просто помните, что если поток зацикливается вокруг очереди работ, он не может делать что-то еще - тогда как обычно основной поток выполнения в консольном приложении предназначен для выполнения последовательности задач, а затем завершения.

Если вы используете .NET 4, очень легко реализовать очередь производителя / потребителя, используя BlockingCollection класс.

person Jon Skeet    schedule 30.01.2011
comment
@Jon: Хорошо, допустим, у меня теперь есть поток с циклом сообщений (который, если я хорошо понимаю ваш ответ, действительно является единственным требованием для вызова делегата между потоками); как мне теперь вызвать делегата в этом конкретном потоке, используя самый общий механизм, предоставляемый .NET Framework / BCL? (Достаточно ли написать свой собственный SynchronizationContext; или мне придется придумывать собственное решение, не имеющее ничего общего с тем, что уже есть в BCL?) - person stakx - no longer contributing; 30.01.2011
comment
@stakx: Это будет зависеть от того, как вы запускали цикл сообщений. Если бы у вас была простая очередь производителя / потребителя, вы могли бы просто добавить сообщение в очередь. - person Jon Skeet; 31.01.2011
comment
Чтобы быстро объяснить, почему я отклонил ваш (полезный) ответ в пользу моего собственного: я беспокоился не столько о циклах сообщений, сколько о поиске наиболее общего способа (класс или интерфейс BCL), который .NET предоставляет для вызова делегата. поперечная резьба. Вот почему я составил список классов и интерфейсов, которые кажутся связанными с этой темой в первую очередь. - Тем не менее, ваш ответ дал мне ценную информацию; а именно, что очереди сообщений действительно не должны быть сложными. - person stakx - no longer contributing; 03.02.2011