Что происходит, когда несколько задач C# запускаются после завершения другой задачи?

Допустим, у меня есть этот код:

public async void Run()
{
    TaskCompletionSource t = new TaskCompletionSource();
    Prepare(t.Task);
    await Task.Delay(1000);

    t.SetResult();
    Console.WriteLine("End");
}

public async void Prepare(Task task)
{
    await Run(task, "A");
    await Run(task, "B");
    await Run(task, "C");
    await Run(task, "D");
}

public async Task Run(Task requisite, string text)
{
    await requisite;
    Console.WriteLine(text);
}

Что происходит, когда вызывается t.SetResult();? Это многопоточность? Есть ли гарантия порядка элементов в Консоли? Если у меня есть List<>, и метод Run меняет его, нужно ли мне беспокоиться о многопоточности?

Перестаньте использовать async void и дождитесь выполнения всех своих задач, и эти вопросы отпадут.

GSerg 01.08.2024 02:45

Это упрощение реального вопроса... Буду признателен, если мы сосредоточимся на ответе, а не на обходных путях...

Everton Elvio Koser 01.08.2024 02:50
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
78
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

  1. При вызове SetResult() первая задача («A») возобновляет выполнение после await внутри функции Run(Task, string) и печатает «A». Затем возвращается от Prepare() к await Run(task, "B"), что печатает «B». И так далее. Наконец, «ABCD» будет напечатан по порядку.
  2. Вы не можете сказать, будет ли он многопоточным или нет. Это будет зависеть от того, что TaskScheduler используется в текущей среде и какие еще задачи выполняются в этом планировщике. По умолчанию у вас будет пул потоков, содержащий несколько потоков. Скорее всего (но без каких-либо гарантий) ваши задачи будут выполняться в разных потоках, а не параллельно (если мы говорим о задачах ABCD).
  3. Порядок ABCD гарантирован, потому что вы выполняете текущую задачу перед тем, как начать следующую.
  4. Насчет изменения await внутри List<> - скорее всего, это сработает, потому что одновременных изменений нет. В то же время это определенно хрупкий код, который можно легко взломать во время будущих модификаций кода. Я постараюсь избегать такого кода, насколько это возможно, и использовать Run или какую-нибудь параллельную коллекцию. Или хотя бы напишите длинный комментарий, описывающий ограничения текущей реализации.

Спасибо! Я думаю, что действительно не могу использовать задачи для своей проблемы... У меня есть некоторая параллельная задача, требующая быстрой синхронизации... Я думаю, мне нужно вручную контролировать фрагментацию метода внутри множества более мелких методов и вручную контролировать их выполнение. ..

Everton Elvio Koser 01.08.2024 03:33

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

ProgrammingLlama 01.08.2024 03:51

Технически, если не обязательно запускать все задачи параллельно (что в любом случае практически невозможно из-за аппаратных ограничений), то можно добиться некоторого параллелизма с Task.WhenAll() и друзьями вместо последовательных await. Итак, вы запускаете кучу задач и ждете их всех. В этом случае вы получите максимально возможный параллелизм (управляемый пулом потоков в зависимости от ресурсов системы).

Serg 01.08.2024 03:51

@EvertonElvioKoser рассмотрите возможность принятия этого ответа и в следующий раз задайте более конкретный вопрос...

Ivan Petrov 01.08.2024 11:27
Ответ принят как подходящий

Пока вы написали простое заявление await;

await requisite;

Компилятор C# перенес ваш метод в отдельный класс и перевел это «простое» await во что-то эквивалентное;

private Task requisite;
private int state = 0;
private void MoveNext()
{
    switch (state)
    {
        case 0:
            var awaiter = requisite.GetAwaiter();
            if (!awaiter.IsCompleted)
            {
                state = 1;
                awaiter.OnCompleted(MoveNext);
                return;
            }
            goto case 1;
        case 1:
            // resume execution here when the task is complete
            break;
    }
}

Как видите, если задача уже завершена, ваш код продолжится синхронно. Если задача была незавершенной, будет зарегистрирован обратный вызов, чтобы продолжить выполнение позже.

Точная реализация каждого awaiter может сильно различаться. Например, Task.Yield вернет ожидающее сообщение, которое никогда не будет завершено. Принудительное выполнение продолжения в пуле потоков.

Большинство типов задач и все задачи из вашего примера будут вызывать каждый метод продолжения синхронно, пока вызывается .SetResult.

Вернемся к вашему конкретному примеру;

public async void Prepare(Task task)
{
    await Run(task, "A");
    await Run(task, "B");
    await Run(task, "C");
    await Run(task, "D");
}

При первом вызове Run задача не завершена, поэтому Run зарегистрирует продолжение Action и вернет неполную задачу. Первый await в методе Prepare также увидит, что возвращенная задача является неполной. Хотя метод void, поэтому еще одна незавершенная задача не возвращается.

Когда вы возобновите работу после задержки и вызовете t.SetResult();, сначала будет вызвано зарегистрированное продолжение для возобновления метода Run. Когда метод Run завершается, а не возвращается, .SetResult будет вызываться для незавершенной задачи, что приведет к немедленному вызову продолжения Prepare. Все это произойдет в одном потоке до того, как t.SetResult вернется.

Если бы вы поместили точку останова в Run или Prepare и проверили стек вызовов, вы бы увидели, как стек потоков инвертируется по сравнению с тем, что вы обычно ожидаете. Каждая возвращающая функция async будет там с вызовом SetResult в стеке.

«Task.Yield вернет ожидающее событие, которое никогда не завершится. Принудительное выполнение продолжения в пуле потоков». -- На самом деле await Task.Yield() продолжается на захваченном SynchronizationContext. Это его документированное поведение. ИМХО, это не пример, подходящий для этого вопроса.

Theodor Zoulias 01.08.2024 06:40

Дело было лишь в том, что официанты могут вести себя по-разному. например, дескрипторы ввода-вывода и другие дескрипторы и таймеры ОС. Но большинство других задач просто немедленно выполняют обратные вызовы.

Jeremy Lakeman 01.08.2024 06:59

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

Но, как правило, Task и использование async/await не гарантирует многопоточность, новые потоки используются (из пула потоков), когда они необходимы - текущий поток занят какой-то работой и не может взять на себя новую работу.

Идея асинхронного выполнения заключается в том, что когда есть работа, которую нужно дождаться (наш процессор должен чего-то ждать, например операцию ввода-вывода), он освобождает текущий поток и позволяет ему выполнить другую работу.

Таким образом, может случиться так, что один поток выполняет две задачи «параллельно». Но это многозадачность, а не многопоточность.

Обладая этими знаниями, давайте проанализируем ваш код.

Во-первых, вы создаете TaskCompletionSource, который имеет Task, который находится в "действии" и его можно ожидать, но он завершится, как только мы вызовем SetResult на TaskCompletionSource.

Затем мы вызываем Prepare, который мы не ждем, но он ожидает прохождения Task, поэтому этот путь выполнения остановится на await Run(task, "A") и будет ждать завершения задачи, что произойдет, когда мы вызовем SetResult (как объяснялось ранее). После Prepare (значит задание уже ожидается) звоним await Task.Delay — у нас тут ещё один await, на этот раз за задержку. Поскольку мы используем Task, новый поток не будет создан, поскольку await Run просто ожидает, поток может продолжить другую работу и обработать выполнение вашего кода. Так что ждем задержки и задача будет выполняться параллельно, асинхронно.

Затем, когда задержка завершится, он установит результат задачи, то есть await Run(task, "A") будет «разблокирован», распечатает результат и продолжит await Run(task, "B"), но при этом будет предпринята попытка дождаться уже выполненной задачи, и поэтому она будет выполняться синхронно. (задание уже выполнено, ждать нечего), сразу печатаем «Б», затем «С», а затем «Д» (последовательно).

Вы можете столкнуться с одной проблемой: ваша программа может завершиться до завершения всех задач в методе Prepare, потому что это async void, и у нас нет возможности ее дождаться. Я бы предложил небольшое улучшение:

public async void Run()
{
    TaskCompletionSource t = new TaskCompletionSource();
    var prepareTask = Prepare(t.Task);
    await Task.Delay(1000);

    t.SetResult();
    
    await prepareTask;
    
    Console.WriteLine("End");
}

public async Task Prepare(Task task)
{
    await Run(task, "A");
    await Run(task, "B");
    await Run(task, "C");
    await Run(task, "D");
}

public async Task Run(Task requisite, string text)
{
    await requisite;
    Console.WriteLine(text);
}

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

Похожие вопросы

ASP .NET Core — расширение @Html.EditorFor для добавления <span>
Netwonsoft JsonConvert.Deserialization выдает исключение JsonSerializationException: «Ошибка преобразования значения «Имя моего пользовательского объекта» в тип «System.Type»
Как обеспечить автоматическое масштабирование для CollectionView в объекте Popup из CommunityToolkit.Maui после его заполнения с помощью события нажатия кнопки на платформе Windows?
Перенаправление вызова .NET Core OIDC не работает с интерфейсом Angular
Как добавить несколько веб-приложений Blazor в существующий веб-API
Веб-приложение .Net 8 Blazor (сервер) -> Начните с языка браузера и разрешите пользователю менять язык через раскрывающийся список
Что означает этот вариант значка проекта CsProj в обозревателе решений Visual Studio 2022?
Передача массива объектов из C# в VBA Excel
Как я могу читать статические свойства из экземпляров классов?
Остановить выполнение приложения при закрытии формы C#