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

StackOverflow https://stackoverflow.com/questions/4843010

Вопрос

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

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

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


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

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

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


То, что я уже знаю:

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

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

    • AsyncOperation & AsyncOperationManager System.ComponentModel)
      Это, кажется, обертки вокруг SynchronizationContext.Понятия не имею, как ими пользоваться.

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

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

    • Dispatcher &DispatcherSynchronizationContext System.Windows.Threading)
      Похоже, что последнее является еще одним подклассом SynchronizationContext, и бывшие делегаты от него.

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

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

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

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

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

Это было полезно?

Решение

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

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

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

Таким образом, целевому потоку нужен какой-то способ "получения" делегатов.Это можно сделать самыми разными способами.Один простой механизм заключается в том, что поток всегда возвращается к некоторому циклу (назовем его "циклом сообщений"), где он будет просматривать очередь.Это сработает с тем, что находится в очереди.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.(Этот контекст синхронизации должен быть ранее зарегистрирован в потоке/by Z.) Затем сохраните эту ссылку где-нибудь, где она доступна по потоку A.

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

Другие советы

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

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

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

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

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

Недавно я наткнулся на эту статью и обнаружил, что она спасает мне жизнь.Использование блокирующей параллельной очереди - это секретный соус, на который указал Джон Скит выше.Лучшее "руководство", которое я нашел по выполнению всей этой работы, - это эта статья в CodeProject Майка Перетца.Статья является частью серии из трех частей, посвященной SynchronizationContext, в которой приведены примеры кода, которые можно легко превратить в рабочий код.Обратите внимание, что Перец не только заполняет все детали, но он также напоминает нам, что базовый SynchronizationContext имеет по сути бесполезные реализации Post() и Send() и, следовательно, действительно должен рассматриваться как абстрактный базовый класс.Обычный пользователь базового класса мог бы быть удивлен, обнаружив, что он не решает реальных проблем.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top