Почему этот асинхронный код не продолжает работать в том же контексте, в котором он ожидался?
Я пытаюсь понять, как работает Task.ContinueWith. Рассмотрим следующий код:
private async void HandleButtonClick(object sender, EventArgs e)
{
Console.WriteLine($"HandleButtonClick: a {GetTrdLabel()}");
var t1 = Task.Run(() => DoSomethingAsync("First time"));
Console.WriteLine($"HandleButtonClick: b {GetTrdLabel()}");
await t1;
Console.WriteLine($"HandleButtonClick: c {GetTrdLabel()}");
var t2 = t1.ContinueWith(async (t) =>
{
Console.WriteLine($"t3: a {GetTrdLabel()}");
Thread.Sleep(2000);
Console.WriteLine($"t3: b {GetTrdLabel()}");
await DoSomethingAsync("Second time");
Console.WriteLine($"t3: c {GetTrdLabel()}");
});
Console.WriteLine($"HandleButtonClick: d {GetTrdLabel()}");
await t2;
Console.WriteLine($"HandleButtonClick: e {GetTrdLabel()}");
}
private async Task DoSomethingAsync(string label)
{
Console.WriteLine($"DoSomethingElseAsync ({label}): a {GetTrdLabel()}");
Thread.Sleep(2000);
Console.WriteLine($"DoSomethingElseAsync ({label}): b {GetTrdLabel()}");
await Task.Delay(2000);
Console.WriteLine($"DoSomethingElseAsync ({label}): c {GetTrdLabel()}");
}
private string GetTrdLabel() => $"({Thread.CurrentThread.ManagedThreadId})";
Выход ниже. Мой вопрос о выделенных строках: почему первый не продолжается в захваченном контексте после await
- т.е. идентификатор управляемого потока 3
- так как я не использовал .ConfigureAwait(false)
? Второй продолжается, как и ожидалось - то есть идентификатор потока 4
,
Я чувствую, что это как-то связано с "... попыткой упорядочить продолжение обратно в исходный захваченный контекст" (выделено мной) из документации, но я не понимаю, почему попытка не удалась в первом случае.
2 ответа
пытаясь понять, как работает Task.ContinueWith
Лучше просто игнорировать ContinueWith
и использовать await
вместо. Но, если вы хотите научиться низкоуровневому, опасному способу делать что-то, то я обязуюсь. Пожалуйста, не используйте в производстве.
Первое, что нужно отметить, это то, что ContinueWith
всегда планирует работу в планировщик задач. И он не использует планировщик задач по умолчанию; по умолчанию используется текущий планировщик задач. При условии, что HandleButtonClick
вызывается непосредственно вашей структурой пользовательского интерфейса (а не, например, запланировано с помощью планировщика задач), тогда нет текущего планировщика задач, поэтому ContinueWith
будет использовать планировщик задач по умолчанию, который является планировщиком задач пула потоков. Чтобы избежать такого рода запутанных рассуждений, код всегда должен передавать TaskScheduler
в ContinueWith
,
Следующее, что следует отметить, это то, что ContinueWith
не понимает async
делегаты. Что касается async
лямбда это просто модный способ дать ему Func<Task>
делегат и ContinueWith
заботится только о начальной синхронной части этого метода.
Последнее, что следует отметить, - потоки пула потоков считаются взаимозаменяемыми. Это верно для любого async
/await
код; если он работает в контексте пула потоков и выполняет await
, он может возобновить выполнение в любом потоке пула потоков. Это может быть или не быть тем же потоком, в котором выполнялся код до await
,
Вы получите разные результаты, если попробуете один и тот же код в приложении Windows. Поток пользовательского интерфейса приложения Windows выполняется в другом контексте, и элемент управления возвращается в поток пользовательского интерфейса после ожидаемой операции. Это не относится к консольным приложениям.