У меня есть код ниже, который должен ждать 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);
}
}
Это не решило проблему — даже если Init равен async Task, он все равно завершается немедленно.
Как ты звонишь Init()? Должно быть await Init();
@RaymondChen, этот код — лишь краткий пример моей проблемы. Я вызываю это из класса модульного тестирования, но на самом деле это все async Task<T>, которое берет начало откуда-то IHostEnvironment
Когда все абсолютно делает свою работу. Его задача — дождаться завершения задач, которые уже поставлены в очередь и в основном запущены.
Конструктор Taskне поддерживает асинхронность (это не лучшая ссылка, но сейчас я не смог найти лучшей).
@TheodorZoulias спасибо за эту ссылку, я прочитал ее и быстро что-то придумал после прочтения ответа Стивена Клири. Код не так аккуратен, как хотелось бы, но свою работу он выполняет.
Есть разница в использовании task.Start() и Task.Run(). Task.Run() начинается сразу, но task.Start() может начаться не сразу. Вместо этого попробуйте использовать Task.Run().
На самом деле это не обман, намерение ОП другое, поэтому обсуждение не ответит на конкретный вопрос.
@ChrisSchaller, проблема со связанным вопросом в том, что он не содержит правильного ответа. В противном случае это похоже на дубликат. Я думаю, что закрытие вопроса и связывание его с дубликатом, на который нет ответа, контрпродуктивно, поэтому я проведу повторное открытое голосование. Я почти уверен, что повторяющиеся вопросы с правильными ответами существуют, но найти их не так уж и просто.
@ChrisSchaller На самом деле благодаря ссылке @TheodorZoulias я разобрался с этим и обновил свой вопрос, чтобы отразить мое решение этой проблемы. Я думаю, что использовал объект Task неправильно, теперь я использовал Func для хранения своих операций на будущее, что тоже работает. Спасибо за вашу помощь.
@ojek, пожалуйста, не включайте ответ в вопрос. Здесь это осуждается. Я бы предложил удалить ответ из вопроса и опубликовать его как правильный ответ.
@TheodorZoulias Я хотел! Но этот вопрос уже закрыт и я больше не могу добавить ответ...
@ojek, это правильная оценка. Func — это операция, которую необходимо выполнить позже, тогда как Task — это обещание завершить ранее начатую (или запланированную) задачу. Задачи используются для асинхронной и параллельной обработки. Хотя вы можете изменить шаблон для создания запланированной работы, если вы это делаете, это часто считается антишаблоном, и для ваших нужд может быть гораздо более простое и элегантное решение.
@ChrisSchaller нет ничего сложного или неэлегантного в создании холодных задач и запуске их на более позднем этапе. Возможно, на вас повлияли труды Стивена Клири. Стивен прав на 98% во всем, что говорит, но его абсолютное неприятие построения «холодных» задач вряд ли основано на каких-либо разумных аргументах. Они ему просто не нравятся. Выставлять холодные задачи из общедоступных API — это большая проблема, но использовать их внутри, особенно в библиотечном коде, вполне нормально. При условии, конечно, что вы знаете, что делаете, и что может быть лучше, чем StackOverflow, чтобы поделиться этими знаниями?
@ojek, я бы все равно предложил удалить ответ из вопроса. Встраивание ответов на вопрос является плохим стилем, нарушает правила сайта и в конечном итоге действует как надгробие, которое испаряет любые шансы на то, что вопрос когда-либо будет открыт повторно. Если вы полны решимости поделиться своим решением всеми возможными способами, вы можете просто добавить ссылку на конкретную версию, которая включает ответ. Не очень агрессивно, и это сохранит ваш вопрос в хорошей форме.





Если вы хотите ждать выполнения задач асинхронно, то не следует использовать 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() абсолютно делает то, что здесь ожидается.
Вы также могли бы использовать таймер или токен отмены, чтобы задержать запуск воркеров, но концептуально шаги те же.
Когда мои задачи начнутся? Мне нужно контролировать время начала этих задач.
В момент добавления — вызов метода запускает задачу.
Я только что протестировал ваш код - myFunc() запускается сразу при добавлении в список задач, спасибо за этот ответ, но он не решит мой вариант использования, в котором необходим task.Start(), поскольку мне нужно выполнить некоторые промежуточные операции, прежде чем фактически запустить задания.
Сделайте перечислимые задачи и передайте их в Task.WhenAll, это будет точка запуска всех задач внутри реализации метода. Или перечисляйте их вручную, когда захотите.
Почему бы сначала не выполнить эти операции? В этом случае ваш пример был слишком упрощен. Когда вы сказали «не работает», вы не упомянули, что ожидаете задержки начала работы, это не проблема, для решения которой предназначен async/await.
@ChrisSchaller, ты прав, я не все объяснил, так как думал, что допустил какую-то простую ошибку, которой не заметил. Я обновил свой вопрос, объясняя, почему мне нужно разделить запуск задач.
Спасибо за ваше редактирование! В итоге я превратил tasks.Add(new Task(...)) в tasks.Add(async () => { await myFunc() }) (теперь tasks — это List<Func<Task>>). Затем я делаю Task.Run() для каждого элемента позже.
Вот как это сделать:
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()));
}
Ключевые моменты:
Task не поддерживает асинхронность. Он не знает, что делать с делегатом async. В конечном итоге делегатом становится async void, а этого следует избегать.Task<Task>. Это вложенная задача, представляющая запуск асинхронной операции. Это не означает завершение асинхронной операции. Внешняя задача завершится сразу после запуска асинхронной операции. Внутренняя задача завершится после завершения асинхронной операции.Task, который представляет как запуск, так и завершение асинхронной операции.scheduler при каждом запуске Task с API StartNew и ContinueWith. Эта же рекомендация применима и к методу Start. В противном случае ваша задача будет запланирована в окружающей среде TaskScheduler.Current, что сделает ваш код зависимым от окружающей среды (в целом это не очень хорошая идея).
Не используйте
async void