Подтвердить что ты не робот

Почему Task.Delay не работает в этой ситуации

Я тестирую async, и я нашел эту ситуацию, которую я не могу понять:

var watch = Stopwatch.StartNew();

var t1 = Task.Factory.StartNew(async () =>
{
    await Task.Delay(2000);

    return 2;
});

var t2 = Task.Factory.StartNew(() =>
{
    Task.Delay(1000);

    return 1; 
});

await Task.WhenAll(t1, t2);

var result = watch.ElapsedMilliseconds;

Я хотел бы понять, почему результат всегда равен 0! Почему не 1000, 2000 или сумма двух задач 3000? Почему Task.WhenAll ждать завершения задач?

4b9b3361

Ответ 1

Итак, второй - простой, поэтому отпустите его.

Для второй задачи t2 вы ничего не делаете с результатом Task.Delay(1000). Вы не await его, вы не Wait и т.д. Учитывая, что метод не является async, я предполагал, что вы предназначались для него как блокирующее ожидание. Чтобы сделать это, вы хотите добавить Wait() в конец вызова Delay, чтобы сделать его ожидающим блокировки, или просто используйте Thread.Sleep().


Для первой задачи вас укусит тот факт, что вы используете var. Ясно, что происходит, когда вы не используете var:

Task<Task<int>> t1 = Task.Factory.StartNew(async () =>
{
    await Task.Delay(2000);

    return 2;
});

Вы возвращаете задачу задачи int, а не только Task из int. Внешняя задача "завершена", как только заканчивается внутренняя задача. Когда вы используете WhenAll, вам все равно, когда закончится внешняя задача, вы заботитесь о завершении внутренней задачи. Существует несколько способов справиться с этим. Один из них - использовать Unwrap.

Task<int> t1 = Task.Factory.StartNew(async () =>
{
    await Task.Delay(2000);

    return 2;
}).Unwrap();

Теперь вы ожидаете, что ожидаемые Task<int> и WhenAll займут не менее 2000 миллисекунд, как и ожидалось. Вы также можете добавить другой вызов await, чтобы сделать то же самое:

Task<int> t1 = await Task.Factory.StartNew(async () =>
{
    await Task.Delay(2000);

    return 2;
});

Как упоминалось svick в комментарии, другим вариантом было бы просто использовать Task.Run вместо StartNew. Task.Run имеет специальный набор перегрузок для методов, которые принимают Func<Task<T>> и возвращают a Task<T> и автоматически разворачивают их для вас:

Task<int> t1 = Task.Run(async () =>
{
    await Task.Delay(2000);

    return 2;
});

По этой причине предпочтительнее использовать Task.Run как параметр по умолчанию при создании async lambdas, поскольку он "обрабатывает" эту проблему для вас, хотя лучше всего знать об этом для сложных случаев, когда вы можете 't использовать Task.Run.


Наконец, мы переходим к тому варианту, который вы не делали, и это то, что вы, вероятно, должны делать в этом случае. Поскольку Task.Delay уже возвращает задачу, нет необходимости вставлять ее в StartNew. Вместо создания вложенной задачи и использования Unwrap вы можете просто не обернуть ее в первую очередь:

var t3 = Task.Delay(3000);
await Task.WhenAll(t1, t2, t3);

Если вы просто хотите дождаться фиксированного количества времени, что вы должны делать.