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

Допустим, вы пишете пользовательскую однопоточную библиотеку GUI (или что-то еще с циклом событий). Из моего понимания, если я использую async/await или просто обычные продолжения TPL, все они будут запланированы на TaskScheduler.Current (или на SynchronizationContext.Current).

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

// All continuation calls should be put onto this queue
Queue<Event> events;

// The main thread calls the `Update` method continuously on each "frame"
void Update() {
    // All accumulated events are processed in order and the queue is cleared
    foreach (var event : events) Process(event);

    events.Clear();
}

Теперь, учитывая мое предположение правильно и TPL использует SynchronizationContext.Current любой код в приложении должен быть в состоянии сделать что-то вроде этого:

async void Foo() {
    someLabel.Text = "Processing";

    await BackgroundTask();

    // This has to execute on the main thread
    someLabel.Text = "Done";
}

Что подводит меня к вопросу. Как реализовать кастом SynchronizationContext что позволило бы мне обрабатывать продолжения в моем собственном потоке? Это даже правильный подход?

1 ответ

Решение

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

По умолчанию, AsyncContext.Run принимает один делегат для выполнения и возвращает, когда он полностью завершен (так как AsyncContext использует обычай SynchronizationContext умеет ждать async void методы, а также обычный асинхронный / синхронизирующий код).

AsyncContext.Run(async () => await DoSomethingAsync());

Если вы хотите больше гибкости, вы можете использовать AsyncContext продвинутые участники (они не отображаются в IntelliSense, но они есть), чтобы поддерживать контекст до появления какого-либо внешнего сигнала (например, "выходной кадр"):

using (var context = new AsyncContext())
{
  // Ensure the context doesn't exit until we say so.
  context.SynchronizationContext.OperationStarted();

  // TODO: set up the "exit frame" signal to call `context.SynchronizationContext.OperationCompleted()`
  // (note that from within the context, you can alternatively call `SynchronizationContext.Current.OperationCompleted()`

  // Optional: queue any work you want using `context.Factory`.

  // Run the context; this only returns after all work queued to this context has completed and the "exit frame" signal is triggered.
  context.Execute();
}

AsyncContext "s Run а также Execute заменить текущий SynchronizationContext пока они работают, но они сохраняют исходный контекст и устанавливают его как текущий перед возвратом. Это позволяет им работать хорошо во вложенном режиме (например, "кадры").

(Я предполагаю, что под "рамкой" вы подразумеваете некий WPF-подобный диспетчерский кадр).

Другие вопросы по тегам