Task.WhenAll не ждет

У меня есть код ниже, который должен ждать 10 секунд. Проблема в том, что он завершается немедленно, метод WhenAll не работает — что я здесь делаю не так?

public class WhenAllIsNotWorking
{
    public async Task myFunc()
    {
        await Task.Delay(10000);
    }

    public async void Init()
    {
        var tasks = new List<Task>();
        for (var i = 0; i < 10; i++)
        {
            tasks.Add(new Task(async () => { await myFunc();  }));
        }
        foreach (var task in tasks)
        {
            task.Start();
        }
        await Task.WhenAll(tasks);
    }
}

Отредактируйте, поскольку я изначально не упоминал об этом - выше приведен упрощенный пример моего реального кода - на самом деле у меня есть иерархическое дерево сущностей, которое я сначала просматриваю и регистрирую операции для каждой сущности (поэтому я использую new Task() с комбинацией task.Start()). После регистрации всех операций я группирую их, а затем выполняю над ними task.Start(), что позволяет мне выполнять операции упорядоченным образом для каждого типа сущности. Конечно, мне бы хотелось это сделать, если бы не тот факт, что WhenAll здесь не выполняет свою работу.

Мое решение: кто-то закрыл мой вопрос, и я больше не могу публиковать ответы, в любом случае вот что я в итоге сделал — спасибо за вашу помощь!

public class WhenAllIsNotWorking
{
    public async Task myFunc()
    {
        await Task.Delay(10000);
    }

    public async Task Init()
    {
        var tasks = new List<Func<Task>>();
        for (var i = 0; i < 10; i++)
        {
            tasks.Add(async () => { await myFunc();  });
        }
        var waitList = new List<Task>();
        foreach (var task in tasks)
        {
            waitList.Add(Task.Run(task));
        }
        await Task.WhenAll(waitList);
    }
}

Не используйте async void

stuartd 17.06.2024 15:38

Это не решило проблему — даже если Init равен async Task, он все равно завершается немедленно.

ojek 17.06.2024 15:45

Как ты звонишь Init()? Должно быть await Init();

Raymond Chen 17.06.2024 15:55

@RaymondChen, этот код — лишь краткий пример моей проблемы. Я вызываю это из класса модульного тестирования, но на самом деле это все async Task<T>, которое берет начало откуда-то IHostEnvironment

ojek 17.06.2024 16:00

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

Chris Schaller 17.06.2024 16:01

Конструктор Taskне поддерживает асинхронность (это не лучшая ссылка, но сейчас я не смог найти лучшей).

Theodor Zoulias 17.06.2024 16:02

@TheodorZoulias спасибо за эту ссылку, я прочитал ее и быстро что-то придумал после прочтения ответа Стивена Клири. Код не так аккуратен, как хотелось бы, но свою работу он выполняет.

ojek 17.06.2024 16:18

Есть разница в использовании task.Start() и Task.Run(). Task.Run() начинается сразу, но task.Start() может начаться не сразу. Вместо этого попробуйте использовать Task.Run().

Martin Staufcik 17.06.2024 16:19

На самом деле это не обман, намерение ОП другое, поэтому обсуждение не ответит на конкретный вопрос.

Chris Schaller 17.06.2024 16:24

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

Theodor Zoulias 17.06.2024 16:32

@ChrisSchaller На самом деле благодаря ссылке @TheodorZoulias я разобрался с этим и обновил свой вопрос, чтобы отразить мое решение этой проблемы. Я думаю, что использовал объект Task неправильно, теперь я использовал Func для хранения своих операций на будущее, что тоже работает. Спасибо за вашу помощь.

ojek 17.06.2024 16:33

@ojek, пожалуйста, не включайте ответ в вопрос. Здесь это осуждается. Я бы предложил удалить ответ из вопроса и опубликовать его как правильный ответ.

Theodor Zoulias 17.06.2024 16:35

@TheodorZoulias Я хотел! Но этот вопрос уже закрыт и я больше не могу добавить ответ...

ojek 17.06.2024 16:35

@ojek, это правильная оценка. Func — это операция, которую необходимо выполнить позже, тогда как Task — это обещание завершить ранее начатую (или запланированную) задачу. Задачи используются для асинхронной и параллельной обработки. Хотя вы можете изменить шаблон для создания запланированной работы, если вы это делаете, это часто считается антишаблоном, и для ваших нужд может быть гораздо более простое и элегантное решение.

Chris Schaller 18.06.2024 01:14

@ChrisSchaller нет ничего сложного или неэлегантного в создании холодных задач и запуске их на более позднем этапе. Возможно, на вас повлияли труды Стивена Клири. Стивен прав на 98% во всем, что говорит, но его абсолютное неприятие построения «холодных» задач вряд ли основано на каких-либо разумных аргументах. Они ему просто не нравятся. Выставлять холодные задачи из общедоступных API — это большая проблема, но использовать их внутри, особенно в библиотечном коде, вполне нормально. При условии, конечно, что вы знаете, что делаете, и что может быть лучше, чем StackOverflow, чтобы поделиться этими знаниями?

Theodor Zoulias 18.06.2024 02:57

@ojek, я бы все равно предложил удалить ответ из вопроса. Встраивание ответов на вопрос является плохим стилем, нарушает правила сайта и в конечном итоге действует как надгробие, которое испаряет любые шансы на то, что вопрос когда-либо будет открыт повторно. Если вы полны решимости поделиться своим решением всеми возможными способами, вы можете просто добавить ссылку на конкретную версию, которая включает ответ. Не очень агрессивно, и это сохранит ваш вопрос в хорошей форме.

Theodor Zoulias 18.06.2024 03:16
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать 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
16
116
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Если вы хотите ждать выполнения задач асинхронно, то не следует использовать await ни на одном из промежуточных шагов, должно сработать следующее упрощение:

public class WhenAllIsNotWorking
{
    public async Task myFunc()
    {
        await Task.Delay(10000);
    }

    // return Task, not void
    public async Task Init()
    {
        var tasks = new List<Task>();
        for (var i = 0; i < 10; i++)
        {
            //tasks.Add(new Task(async () => { await myFunc();  }));
            tasks.Add(myFunc());
        }
        // Tasks should start immediately
        //foreach (var task in tasks)
        //{
        //    task.Start();
        //}
        await Task.WhenAll(tasks);
    }
}

Обновлять

Если проблема в том, что вы намеренно хотите предотвратить запуск задачи до определенного момента времени, то async/await специально не предназначен для решения этой проблемы.

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

https://dotnetfiddle.net/V9J3Np

public class WhenAllIsNotWorking
{
    // return Task, not void
    public async Task Init()
    {
        Console.WriteLine("Begin Init()");
        var workers = new List<DoSomeWork>();
        for (var i = 0; i < 10; i++)
        {
            workers.Add(new DoSomeWork(i));
        }
        Console.WriteLine("Workers Created...");
        
        foreach (var worker in workers)
        {
            worker.Prepare();
        }
        Console.WriteLine("Workers Prepared...");
        
        Console.WriteLine("Tasks Started...");
        await Task.WhenAll(workers.Select(x => x.Start()));
        Console.WriteLine("Tasks Completed...");
    }
}

public class DoSomeWork
{
    public int Id { get;set; }
    public bool Prepared { get; private set; }
    
    public DoSomeWork(int id)
    {
        this.Id = id;   
    }

    public void Prepare() { this.Prepared = true; }
    
    public async Task Start()
    {
        await Task.Delay(1000);
        Console.WriteLine("Completed Task: {0}", Id);
    }
}

Task.WhenAll() абсолютно делает то, что здесь ожидается.

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

Когда мои задачи начнутся? Мне нужно контролировать время начала этих задач.

ojek 17.06.2024 15:49

В момент добавления — вызов метода запускает задачу.

Ryan 17.06.2024 15:52

Я только что протестировал ваш код - myFunc() запускается сразу при добавлении в список задач, спасибо за этот ответ, но он не решит мой вариант использования, в котором необходим task.Start(), поскольку мне нужно выполнить некоторые промежуточные операции, прежде чем фактически запустить задания.

ojek 17.06.2024 15:53

Сделайте перечислимые задачи и передайте их в Task.WhenAll, это будет точка запуска всех задач внутри реализации метода. Или перечисляйте их вручную, когда захотите.

Ryan 17.06.2024 15:57

Почему бы сначала не выполнить эти операции? В этом случае ваш пример был слишком упрощен. Когда вы сказали «не работает», вы не упомянули, что ожидаете задержки начала работы, это не проблема, для решения которой предназначен async/await.

Chris Schaller 17.06.2024 15:58

@ChrisSchaller, ты прав, я не все объяснил, так как думал, что допустил какую-то простую ошибку, которой не заметил. Я обновил свой вопрос, объясняя, почему мне нужно разделить запуск задач.

ojek 17.06.2024 16:01

Спасибо за ваше редактирование! В итоге я превратил tasks.Add(new Task(...)) в tasks.Add(async () => { await myFunc() }) (теперь tasks — это List<Func<Task>>). Затем я делаю Task.Run() для каждого элемента позже.

ojek 17.06.2024 16:27
Ответ принят как подходящий

Вот как это сделать:

public async Task Init()
{
    List<Task<Task>> tasks = new();

    for (int i = 0; i < 10; i++)
    {
        tasks.Add(new Task<Task>(() => myFunc()));
    }

    // Do some operations in-between, before actually starting the tasks.

    foreach (Task<Task> taskTask in tasks)
    {
        taskTask.Start(TaskScheduler.Default);
    }

    // Wait for all the tasks to complete
    await Task.WhenAll(tasks.Select(t => t.Unwrap()));
}

Ключевые моменты:

  1. Неуниверсальный Task не поддерживает асинхронность. Он не знает, что делать с делегатом async. В конечном итоге делегатом становится async void, а этого следует избегать.
  2. Вы можете назначить асинхронный делегат для Task<Task>. Это вложенная задача, представляющая запуск асинхронной операции. Это не означает завершение асинхронной операции. Внешняя задача завершится сразу после запуска асинхронной операции. Внутренняя задача завершится после завершения асинхронной операции.
  3. Вы можете использовать метод Unwrap для создания нового прокси Task, который представляет как запуск, так и завершение асинхронной операции.
  4. Microsoft рекомендует всегда настраивать аргумент scheduler при каждом запуске Task с API StartNew и ContinueWith. Эта же рекомендация применима и к методу Start. В противном случае ваша задача будет запланирована в окружающей среде TaskScheduler.Current, что сделает ваш код зависимым от окружающей среды (в целом это не очень хорошая идея).

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