Следующая программа выполняет обе WriteLine
, хотя задача никогда не ожидается:
var nonAwaitedTask = Foo();
Thread.Sleep(10);
static async Task Foo()
{
Console.WriteLine("before delay");
await Task.Delay(1);
Console.WriteLine("after delay");
}
При удалении Thread.Sleep
второй WriteLine
не выполняется.
Является ли это состоянием гонки, даже если должен быть задействован только основной поток? Когда и почему (основной) поток продолжает выполнение кода async
(MoveNext
)?
Я мог видеть несколько сценариев, в которых может быть выполнен WriteLine
, например:
await Task.Delay(0)
(awaiter.IsCompleted
сразу true
и Foo
не уступит).await Task.Delay(1).ConfigureAwait(false)
(другой поток мог запустить код, пока основной поток блокируется на Thread.Sleep
)await
(например, Task.Delay(10)
) в Main
(вместо блокировки на Thread.Sleep
), что позволит Foo
работать одновременно.Я понимаю, что последние два будут условиями гонки и что первая оптимизация компилятора может быть не гарантирована (короче говоря, просто await
ваши задачи).
Однако моя ментальная модель, в которой участвует только один поток и где этот поток может продолжать выполнение только в четко определенных (await
) точках, похоже, не выдерживает критики.
Любые объяснения приветствуются!
Спасибо, Рэймонд Чен! Я все еще могу наблюдать такое же поведение с [STAThread]
.
Если это вся ваша программа, то вы можете назвать это своего рода состоянием гонки между закрытием процесса и завершением Foo
. Вы можете переписать приложение на:
var nonAwaitedTask = Foo();
Console.ReadKey();
// ...
С тем же эффектом, что и Thread.Sleep(10);
.
Task.Delay(1);
— это так называемая «действительно асинхронная» операция, и, поскольку кажется, что вы запускаете консольное приложение без контекста синхронизации (следовательно, ConfigureAwait
не будет иметь никакого эффекта), поэтому продолжение (Console.WriteLine("after delay");
) будет запланировано для любого потока пула потоков. await
-ing не влияет на завершение задачи, он влияет на то, что вызывающий метод может ожидать/наблюдать за ним.
var nonAwaitedTask = Foo();
— это задача «выстрелил и забыл».
Читать далее:
Спасибо Гуру Строн! Меня смущает тот факт, что у основного потока, по-видимому, никогда не будет возможности продолжить выполнение (он «занят» для всей программы, поскольку все вызовы блокируются), и у меня сложилось впечатление, что никакой другой поток не может запустить код (например, без .ConfigureAwait(false)).
@dsybot Как я написал в ответе (и это описано в некоторых ссылках), ConfigureAwait
влияет на работу с контекстом синхронизации (что может повлиять на то, какой поток используется для продолжения). Не во всех приложениях он есть, по умолчанию контекст синхронизации отсутствует в консольных приложениях или в современных ASP.NET Core (но есть в приложениях с пользовательским интерфейсом, таких как WPF или WinForms).
@dsybot также проверьте Не блокировать асинхронный код и Не блокировать асинхронный код
Еще раз спасибо Гуру Строн! Я пропустил часть о (отсутствующем) контексте синхронизации; в коде, над которым я работаю, он существует, я попытался извлечь некоторый код, который я исследовал, в консольное приложение, и был удивлен поведением. Когда разрешено несколько потоков, на самом деле это не вопрос, и он ведет себя так, как ожидалось!
Приложения пользовательского интерфейса @dsybot допускают несколько потоков, дело в том, что только один (пользовательский интерфейс) может изменять графический интерфейс. Таким образом, вопрос, где выполняется продолжение, становится важным (отсюда контекст синхронизации и ConfigureAwait
). P.S. если какой-либо из ответов работает для вас - не стесняйтесь отмечать его как принятый.
Спасибо Гуру Строн! Я работал в приложении с контекстом синхронизации, хотя в моем примере кода его не было: я не знал о том, что привело к этому вопросу и всем точкам маркера «различные сценарии»! Поскольку здесь нет контекста потока, все это имеет смысл! Проголосовали за оба, ответы, высоко оценены!
Я предполагаю, что вы работаете в консольной программе. Это несколько отличается от программы пользовательского интерфейса.
static async Task Main(string[] args)
, чтобы избежать этого.Является ли это состоянием гонки, даже если должен быть задействован только основной поток?
Да, если вы используете await в консольной программе, будет задействовано несколько потоков, если вы не сделаете что-то, чтобы переопределить планировщик.
Я бы рекомендовал использовать термин «поток пользовательского интерфейса», поскольку он менее двусмыслен, чем «основной поток». В консольной программе основной поток ничем не отличается от произвольного потока пула потоков.
Когда и почему (основной) поток продолжает выполнение асинхронного кода (MoveNext)?
Компилятор перепишет Foo
так, чтобы он возвращал задачу при первом ожидании. В этот момент вызывающая сторона может продолжить работу с другими вещами, если только она не ожидает возвращенной задачи.
В конце концов, я бы рекомендовал использовать асинхронный основной метод и убедиться, что все задачи ожидаются. Это должно немного помочь избежать некоторых проблем с потоками.
This is somewhat different compared to a UI program
Спасибо JonasH, это было ключом! Итак, для приложений пользовательского интерфейса (или, например, приложений Unity) вторая печать не будет выполнена? Если в консольных приложениях нет контекста потока, наблюдаемое поведение щелкает!
@dsybot В приложении пользовательского интерфейса будет напечатано «до задержки», затем поток пользовательского интерфейса будет бездействовать в течение 10 секунд, и только после этого он должен напечатать «после задержки».
Еще раз спасибо JonasH! Я проделал плохую работу по переводу кода, который я просматривал, в образец (эквивалентным было бы то, что приложение завершило работу, как консольное приложение), и я был удивлен, увидев такое поведение в образце консоли! Ваш ответ мне очень помог!
Проголосовал за ваш ответ, не знаю, почему он до сих пор показывает 0 голосов!
Установите точку останова на второй строке Write и обратите внимание, что она выполняется в другом потоке. Это связано с тем, что задача выполняется в многопоточном апартаменте, поэтому ее можно возобновить в любом фоновом потоке. Если вы пометите свою основную функцию как [STAThread], она будет работать только в этом потоке.