SynchronizationContext
и TaskScheduler
- две абстракции, управляющие тем, где (в каком потоке) будет исполнен код, вызванный асинхронно.
Разница в том, что SynchronizationContext
управляет тем, где (в каком потоке) будет вызван Continuation
(то есть, например, код после await
), а TaskScheduler
определяет, где (в каком потоке) будет выполнен само тело Task
, т.е. где будет выполнен код делегата, который мы передаём в конструктор Task
.
Рассмотрим пример.
Создадим приложение WindowsForms
.
Мы знаем, что WindowsForms
при создании главной формы устанавливает WindowsFormsSynchronizationContext
в качестве Current
для текущего потока (это главный, т.е. UI поток).
Метод WindowsFormsSynchronizationContext.Post
реализован следующим образом:
public override void Post(SendOrPostCallback d, object state) {
controlToSendTo?.BeginInvoke(d, new object[] { state });
}
Значит, код после await
будет направляться на выполнение в UI поток.
Реализуем свой глупенький TaskScheduler
, который запоминает только последний Task
, переданный ему, чтобы поставить в очередь, и раз в пять секунд запускает задачу из "очереди", состоящей из одного элемента. В нём нет практического смысла, просто для демонстрации:
class MyTaskScheduler : TaskScheduler {
Task? scheduledTask;
object lockObj = new();
System.Timers.Timer t;
public MyTaskScheduler()
{
t = new(5000);
t.Elapsed += (o, e) => StartTask();
}
protected override IEnumerable<Task>? GetScheduledTasks() {
lock (lockObj) {
return scheduledTask != null ? new List<Task> {scheduledTask} : new List<Task>(0);
}
}
protected override void QueueTask(Task task) {
lock (lockObj) {
scheduledTask = task;
Debug.WriteLine($"Task {task.Id} scheduled");
if (!t.Enabled)
t.Start();
}
}
void StartTask() {
lock (lockObj) {
if (scheduledTask != null)
base.TryExecuteTask(scheduledTask);
}
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) {
return false;
}
}
В обрабочике клика запустим Task
, передав экземпляр нашего планировщика в соответствующем параметре метода Task.Factory.StartNew
:
public partial class Form1 : Form {
MyTaskScheduler scheduler = new();
static int counter = 0;
public Form1() {
InitializeComponent();
// раскоментируй, чтобы присвоение текста после await
// вызывало Exception (доступ не из UI Thread)
// SynchronizationContext.SetSynchronizationContext(null);
}
private async void button1_Click(object sender, EventArgs e) {
Debug.WriteLine($"UI Therad ID: {Thread.CurrentThread.ManagedThreadId}");
counter++;
Task<string> t = Task.Factory.StartNew(() => {
Debug.WriteLine($"Task {Task.CurrentId} executing in Therad: "
+ "{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(3000);
//button1.Text = "!!!"; // Exception - доступ не из UI потока!
return counter.ToString();
}, CancellationToken.None, TaskCreationOptions.None, scheduler);
// Сработал Synchronization Context и continuation выполняется в UI потоке
button1.Text = await t;
Debug.WriteLine($"Continuation for Task {t.Id} in Therad: "
+ "{Thread.CurrentThread.ManagedThreadId}");
}
}
Сразу после нажатия кнопки в окне Output в Visual Studio видим сообщения о том, что ID
UI-потока - 1, что наш Task
поставлен в очередь.
Через пять секунд увидим, что наш Task
выполнился в другом потоке.
Ещё через три секунды увидим, что продолжение метода button1_Click
(код после await
) выполнился в UI-потоке, и текст кнопки был изменён.
То есть, тем, где выплолнять сам Task
, ведает TaskScheduler
, а тем где выполнять продолжение - SynchronizationContext
.