жду vs Task.Wait - тупик?

197

Я не совсем понимаю разницу между Task.Waitиawait .

У меня есть что-то похожее на следующие функции в службе ASP.NET WebAPI:

public class TestController : ApiController
{
    public static async Task<string> Foo()
    {
        await Task.Delay(1).ConfigureAwait(false);
        return "";
    }

    public async static Task<string> Bar()
    {
        return await Foo();
    }

    public async static Task<string> Ros()
    {
        return await Bar();
    }

    // GET api/test
    public IEnumerable<string> Get()
    {
        Task.WaitAll(Enumerable.Range(0, 10).Select(x => Ros()).ToArray());

        return new string[] { "value1", "value2" }; // This will never execute
    }
}

Где Getбудет тупик.

Что может вызвать это? Почему это не вызывает проблемы, когда я использую блокирующее ожидание, а не await Task.Delay?

ronag
источник
@Servy: я вернусь с репо, как только у меня будет время. На данный момент это работает, Task.Delay(1).Wait()что достаточно хорошо.
Ронаг
2
Task.Delay(1).Wait()в основном то же самое, что и Thread.Sleep(1000). В реальном производственном коде это редко уместно.
Servy
@ronag: Ваш WaitAllвызывает тупик. Смотрите ссылку на мой блог в моем ответе для более подробной информации. Вы должны использовать await Task.WhenAllвместо этого.
Стивен Клири
6
@ronag Потому что у вас есть ConfigureAwait(false)в единый вызов Barили Rosне тупик, а потому , что у вас есть перечислимых , что создает больше , чем один , а затем ждать на всех тех, первая полоса будет тупиковой второй. Если вы await Task.WhenAllвместо того, чтобы ждать выполнения всех задач, чтобы не блокировать контекст ASP, вы увидите, что метод возвращает нормально.
Servy
2
@ronag Ваш другой вариант - добавить .ConfigureAwait(false) весь путь вверх по дереву, пока вы не заблокируете, чтобы ничто не пыталось вернуться к основному контексту; это будет работать. Другим вариантом будет ускорение внутреннего контекста синхронизации. Link . Если вы вставите Task.WhenAllв него, AsyncPump.Runон будет эффективно блокировать все это без необходимости ConfigureAwaitгде-либо, но это, вероятно, слишком сложное решение.
Servy

Ответы:

270

Waitи await- хотя схожи концептуально - на самом деле совершенно разные.

Waitбудет синхронно блокироваться, пока задача не будет завершена. Таким образом, текущий поток буквально блокируется в ожидании завершения задачи. Как правило, вы должны использовать « asyncвесь путь вниз»; то есть не блокируйте asyncкод. В своем блоге я подробно расскажу о том, как блокировка в асинхронном коде вызывает тупик .

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

Вы также упомянули «кооперативный блок», под которым я предполагаю, что вы имеете в виду задачу, которую вы Waitвыполняете, может выполняться в ожидающем потоке. Есть ситуации, когда это может произойти, но это оптимизация. Есть много ситуаций, когда это не может произойти, например, если задача предназначена для другого планировщика, или если она уже запущена, или если это не кодовая задача (например, в вашем примере кода: Waitневозможно выполнитьDelay задачу встроенным, потому что нет кода для этого).

Вы можете найти мое async/ awaitвступление полезным.

Стивен Клири
источник
5
Я думаю, что есть недоразумение, Waitотлично работает awaitтупиков.
Ронаг
1
Ясно: Да, если я заменить await Task.Delay(1)с Task.Delay(1).Wait()сервисом работает нормально, в противном случае это ТУПИКИ.
Ронаг
5
Нет, планировщик задач этого не сделает. Waitблокирует поток, и его нельзя использовать для других целей.
Стивен Клири
8
@ronag Полагаю, вы просто перепутали имена методов, и ваша тупиковая ситуация была вызвана кодом блокировки и работала с awaitкодом. Либо это, либо тупик не был связан ни с чем, и вы неправильно диагностировали проблему.
Servy
3
@hexterminator: Это сделано специально - он отлично работает для приложений пользовательского интерфейса, но, как правило, мешает приложениям ASP.NET. ASP.NET Core исправил это, удалив SynchronizationContextблокировку в запросе ASP.NET Core, которая больше не блокируется.
Стивен Клири
5

На основании того, что я прочитал из разных источников:

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

Чтобы дождаться taskзавершения сингла , вы можете вызвать его Task.Waitметод. Вызов Waitметода блокирует вызывающий поток до тех пор, пока один экземпляр класса не завершит выполнение. Метод без параметров Wait()используется для безусловного ожидания завершения задачи. Задача имитирует работу, вызывая Thread.Sleepметод для сна в течение двух секунд.

Эта статья также хорошо читается.

Ayushmati
источник
3
"Разве это не технически неверно? Может, кто-нибудь прояснит?" - могу я уточнить; ты спрашиваешь это как вопрос? (Я просто хочу уточнить, спрашиваете ли вы против ответа). Если вы спрашиваете: это может работать лучше как отдельный вопрос; вряд ли здесь соберутся новые ответы в качестве ответа
Марк Грэвелл
1
Я ответил на вопрос и задал отдельный вопрос по поводу сомнений, которые у меня были здесь stackoverflow.com/questions/53654006/… Спасибо @MarcGravell. Можете ли вы удалить свой голос за удаление за ответ сейчас?
Аюшмати
"Можете ли вы удалить свой голос за удаление за ответ сейчас?" - это не мое; Благодаря ♦, любое такое голосование мной вступило бы в силу немедленно. Однако я не думаю, что это отвечает на ключевые вопросы, касающиеся тупикового поведения.
Марк Гравелл
Это неправда. Пока первое ожидание не достигнуто все заблокировано
user1785960
-2

Некоторые важные факты не были приведены в других ответах:

«async await» более сложен на уровне CIL и, следовательно, стоит памяти и процессорного времени.

Любая задача может быть отменена, если время ожидания недопустимо.

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

Использование Task более гибкое, чем «async await».

Любые функции синхронизации могут быть упакованы с помощью async.

public async Task<ActionResult> DoAsync(long id) 
{ 
    return await Task.Run(() => { return DoSync(id); } ); 
} 

«асинхронное ожидание» порождает много проблем. Мы пока не ожидаем, что заявление будет достигнуто без выполнения и отладки контекста. Если первое ожидание не достигнуто, все блокируется . Иногда даже кажется, что ожидание достигнуто, но все заблокировано:

https://github.com/dotnet/runtime/issues/36063

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

Вывод: создавать задачи вручную и контролировать их гораздо лучше. Обработчик задачи дает больше контроля. Мы можем отслеживать задачи и управлять ими:

https://github.com/lsmolinski/MonitoredQueueBackgroundWorkItem

Извините за мой английский.

user1785960
источник