Изменение SynchronizationContext в асинхронном методе - PullRequest
0 голосов
/ 10 апреля 2019

Я пытаюсь опубликовать простой вопрос здесь, без кода, потому что мой вопрос настолько специфичен: возможно / приемлемо ли изменить SynchronizationContext в методе Async? Если я не установлю SynchronizationContext при запуске метода Async, кажется, что код в нем - включая события, которые я вызываю, и методы в том же модуле класса, который я вызываю, - запускается в том же рабочем потоке. Однако, когда приходит время для взаимодействия с пользовательским интерфейсом, я обнаруживаю, что SynchronizationContext должен быть установлен в поток пользовательского интерфейса.

Можно ли оставить SynchronizationContext установленным для рабочего потока до тех пор, пока я не захочу вызвать вызов функции на основе пользовательского интерфейса?

EDIT: Чтобы прояснить мой вышеупомянутый вопрос, я нахожусь в безвыходной ситуации с настройкой SynchronizationContext. Если не установить SynchronizationContext, то мои асинхронные операции выполняются в отдельном потоке (по желанию), но тогда я не могу вернуть данные в поток пользовательского интерфейса, не встретив исключение операции между потоками; если я устанавливаю SynchronizationContext для своего потока пользовательского интерфейса, то операции, которые я хочу выполнить в отдельном потоке, в конечном итоге выполняются в потоке пользовательского интерфейса - и затем (конечно) я избегаю исключения между потоками, и все работает. Очевидно, я что-то упустил.

Если вы хотите прочитать больше, я попытался дать очень четкое объяснение того, что я пытаюсь сделать, и я понимаю, что вы тратите свое время, чтобы понять, поэтому спасибо вам за это!

То, что я пытаюсь сделать, показано на этой блок-схеме:

enter image description here

У меня есть приложение Winforms, работающее в потоке пользовательского интерфейса (черным цветом); У меня есть объект Socket, который я хотел бы запустить в своем собственном потоке. Работа класса сокетов заключается в считывании данных из коммуникационного сокета и возврате событий обратно в пользовательский интерфейс при получении данных.

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

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

if (Socket.IsConnected)
{
   SetUpEventListeners();
   // IMPORTANT: next line seems to be required in order to avoid a cross-thread error
   SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
   Socket.StartMessageLoopAsync();
}

Когда я запускаю цикл обработки сообщений из потока пользовательского интерфейса, я вызываю асинхронный метод для объекта Socket:

 public async void StartMessageLoopAsync()
  {
     while (true)
     {
        // read socket data asynchronously and populate the global DataBuffer
        await ReadDataAsync();

        if (DataBuffer.Count == 0)
        {
           OnDataReceived();
        }
     }
  }

Объект Socket также имеет метод OnDataReceived(), определенный как:

  protected void OnDataReceived()
  {
     var dataEventArgs = new DataEventArgs();
     dataEventArgs.DataBuffer = DataBuffer;

     // *** cross-thread risk here!
     DataReceived?.Invoke(this, dataEventArgs);
  }

Я выделил две области диаграммы с помощью синих звездочек «1» и «2».

В «1» (на диаграмме) я использую шаблон async / await. Я использую сторонний инструмент для сокетов, который не поддерживает Async, поэтому я включил его в свою собственную ReadDataAsync() функцию, которая выглядит следующим образом:

  public override async Task ReadDataAsync()
  {
     // read a data asynchronously
     var task = Task.Run(() => ReadData());
     await task;
  }

ReadData() оборачивает метод чтения стороннего компонента: он заполняет глобальный буфер данных, поэтому возвращаемое значение не требуется.

В "2" на диаграмме я сталкиваюсь с перекрестным риском, описанным в моем OnDataReceived() методе, указанном выше.

Итог: если я установлю SynchronizationContext, как показано в моем первом фрагменте кода выше, то все в объекте Socket будет работать в своем собственном потоке, пока я не попытаюсь вызвать обработчик события DataReceived; если я закомментирую SynchronizationContext, то единственная часть кода, которая выполняется в своем собственном потоке, - это краткая сторонняя операция чтения сокетов, заключенная в моем методе DataReadAsync().

Поэтому я подумал, могу ли я установить SynchronizationContext перед попыткой вызова обработчика событий DataReceived. И даже если я "могу", лучший вопрос - хорошая ли это идея? Если я изменю SynchronizationContext в потоке, не являющемся пользовательским интерфейсом, то мне придется вернуть его к исходному значению после вызова метода DataReceived, и это будет иметь дуро-подобный запах кода для меня.

Есть ли в моем дизайне элегантная настройка или он нуждается в капитальном ремонте? Моя цель состоит в том, чтобы все красные элементы на диаграмме работали в потоке без пользовательского интерфейса, а черные элементы - в потоке пользовательского интерфейса. «2» - это точка, в которой поток без пользовательского интерфейса переходит к потоку пользовательского интерфейса ...

Спасибо.

Ответы [ 2 ]

1 голос
/ 10 апреля 2019

Установка контекста напрямую - не лучшая идея, так как это может повлиять на другие функции, выполняемые в этой теме Наиболее естественный способ управления контекстом синхронизации для потоков async/await - использование ConfigureAwait. Так что в вашем случае я вижу два варианта для достижения того, что вы хотите:

1) Использование ConfigureAwait(false) с ReadDataAsync

public async void StartMessageLoopAsync()
{
    while (true)
    {
        // read socket data asynchronously and populate the global DataBuffer
        await ReadDataAsync().ConfigureAwait(false);

        if (DataBuffer.Count == 0)
        {
            OnDataReceived();
        }
    }
}

, который заставит возобновить все после ожидания в фоновом потоке. А затем используйте Dispatcher Invoke для маршала DataReceived?.Invoke в поток пользовательского интерфейса:

protected void OnDataReceived()
{
   var dataEventArgs = new DataEventArgs();
   dataEventArgs.DataBuffer = DataBuffer;

   // *** cross-thread risk here!
   Dispatcher.CurrentDispathcer.Invoke(() => { DataReceived?.Invoke(this, dataEventArgs ); });
}

Или 2) Произведите некоторую логическую декомпозицию следующим образом:

    public async void StartMessageLoopAsync()
    {
        while (true)
        {
            // read socket data asynchronously and populate the global DataBuffer
            await ProcessDataAsync();

            // this is going to run in UI thread but there is only DataReceived invocation
            if (DataBuffer.Count == 0)
            {
                OnDataReceived();
            }
        }
    }

OnDataReceived теперь тонкий и выполняет только запуск событий

    protected void OnDataReceived()
    {
        // *** cross-thread risk here!
        DataReceived?.Invoke(this, dataEventArgs);
    }

Это объединяет функциональность, которая должна выполняться в фоновом потоке

    private async Task ProcessDataAsync()
    {
        await ReadDataAsync().ConfigureAwait(false);

        // this is going to run in background thread
        var dataEventArgs = new DataEventArgs();
        dataEventArgs.DataBuffer = DataBuffer;
    }

    public override async Task ReadDataAsync()
    {
        // read a data asynchronously
        var task = Task.Run(() => ReadData());
        await task;
    }
0 голосов
/ 10 апреля 2019

Этот сценарий лучше подходит для реактивных расширений:

Реактивные расширения для .NET

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