Почему эта программа на С# выполняет операторы после ожидания?

Следующая программа выполняет обе 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) точках, похоже, не выдерживает критики.

Любые объяснения приветствуются!

Установите точку останова на второй строке Write и обратите внимание, что она выполняется в другом потоке. Это связано с тем, что задача выполняется в многопоточном апартаменте, поэтому ее можно возобновить в любом фоновом потоке. Если вы пометите свою основную функцию как [STAThread], она будет работать только в этом потоке.

Raymond Chen 04.04.2023 16:17

Спасибо, Рэймонд Чен! Я все еще могу наблюдать такое же поведение с [STAThread].

dsybot 04.04.2023 17:03
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
2
92
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Если это вся ваша программа, то вы можете назвать это своего рода состоянием гонки между закрытием процесса и завершением 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 04.04.2023 16:41

@dsybot Как я написал в ответе (и это описано в некоторых ссылках), ConfigureAwait влияет на работу с контекстом синхронизации (что может повлиять на то, какой поток используется для продолжения). Не во всех приложениях он есть, по умолчанию контекст синхронизации отсутствует в консольных приложениях или в современных ASP.NET Core (но есть в приложениях с пользовательским интерфейсом, таких как WPF или WinForms).

Guru Stron 04.04.2023 16:44

Еще раз спасибо Гуру Строн! Я пропустил часть о (отсутствующем) контексте синхронизации; в коде, над которым я работаю, он существует, я попытался извлечь некоторый код, который я исследовал, в консольное приложение, и был удивлен поведением. Когда разрешено несколько потоков, на самом деле это не вопрос, и он ведет себя так, как ожидалось!

dsybot 04.04.2023 17:10

Приложения пользовательского интерфейса @dsybot допускают несколько потоков, дело в том, что только один (пользовательский интерфейс) может изменять графический интерфейс. Таким образом, вопрос, где выполняется продолжение, становится важным (отсюда контекст синхронизации и ConfigureAwait). P.S. если какой-либо из ответов работает для вас - не стесняйтесь отмечать его как принятый.

Guru Stron 04.04.2023 17:13

Спасибо Гуру Строн! Я работал в приложении с контекстом синхронизации, хотя в моем примере кода его не было: я не знал о том, что привело к этому вопросу и всем точкам маркера «различные сценарии»! Поскольку здесь нет контекста потока, все это имеет смысл! Проголосовали за оба, ответы, высоко оценены!

dsybot 04.04.2023 17:35

Я предполагаю, что вы работаете в консольной программе. Это несколько отличается от программы пользовательского интерфейса.

  1. Если вы используете обычную пустую основную программу, вы рискуете выйти из программы до того, как все задачи будут завершены. Вы можете использовать «асинхронный основной», то есть static async Task Main(string[] args), чтобы избежать этого.
  2. Нет контекста пользовательского интерфейса, который гарантирует, что все выполняется в одном потоке. Вместо этого все после ожидания будет выполняться в каком-то произвольном потоке пула потоков.

Является ли это состоянием гонки, даже если должен быть задействован только основной поток?

Да, если вы используете await в консольной программе, будет задействовано несколько потоков, если вы не сделаете что-то, чтобы переопределить планировщик.

Я бы рекомендовал использовать термин «поток пользовательского интерфейса», поскольку он менее двусмыслен, чем «основной поток». В консольной программе основной поток ничем не отличается от произвольного потока пула потоков.

Когда и почему (основной) поток продолжает выполнение асинхронного кода (MoveNext)?

Компилятор перепишет Foo так, чтобы он возвращал задачу при первом ожидании. В этот момент вызывающая сторона может продолжить работу с другими вещами, если только она не ожидает возвращенной задачи.

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

This is somewhat different compared to a UI program Спасибо JonasH, это было ключом! Итак, для приложений пользовательского интерфейса (или, например, приложений Unity) вторая печать не будет выполнена? Если в консольных приложениях нет контекста потока, наблюдаемое поведение щелкает!
dsybot 04.04.2023 17:06

@dsybot В приложении пользовательского интерфейса будет напечатано «до задержки», затем поток пользовательского интерфейса будет бездействовать в течение 10 секунд, и только после этого он должен напечатать «после задержки».

JonasH 04.04.2023 17:10

Еще раз спасибо JonasH! Я проделал плохую работу по переводу кода, который я просматривал, в образец (эквивалентным было бы то, что приложение завершило работу, как консольное приложение), и я был удивлен, увидев такое поведение в образце консоли! Ваш ответ мне очень помог!

dsybot 04.04.2023 17:29

Проголосовал за ваш ответ, не знаю, почему он до сих пор показывает 0 голосов!

dsybot 04.04.2023 17:35

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