Как мне создать собственный 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-подобный диспетчерский кадр).