Пример async / await, вызывающий тупик

97

Я столкнулся с некоторыми передовыми практиками асинхронного программирования с использованием ключевых слов async/ awaitслов в C # (я новичок в C # 5.0).

Один из полученных советов был следующим:

Стабильность: знайте свои контексты синхронизации

... Некоторые контексты синхронизации не реентерабельные и однопоточные. Это означает, что только одна единица работы может быть выполнена в контексте в данный момент времени. Примером этого является поток пользовательского интерфейса Windows или контекст запроса ASP.NET. В этих контекстах однопоточной синхронизации легко зайти в тупик. Если вы запускаете задачу из однопоточного контекста, а затем ожидаете ее выполнения в контексте, ваш код ожидания может блокировать фоновую задачу.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

Если я попытаюсь проанализировать его сам, основной поток порождает новый MyWebService.GetDataAsync();, но, поскольку основной поток ждет там, он ожидает результата в GetDataAsync().Result. Между тем говорят, что данные готовы. Почему основной поток не продолжает свою логику продолжения и не возвращает строковый результат GetDataAsync()?

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

Дрор Вайс
источник
Вы действительно уверены, что GetDataAsync завершает работу? Или он застревает, вызывая только блокировку, а не тупик?
Андрей
Это предоставленный пример. Насколько я понимаю, он должен закончить свое дело и иметь какой-то готовый результат ...
Дрор Вайс,
4
Почему ты вообще ждешь задания? Вместо этого вам следует ждать, потому что вы в основном потеряли все преимущества асинхронной модели.
Тони Петрина
Чтобы добавить к пункту @ ToniPetrina, даже без проблемы взаимоблокировки, var data = GetDataAsync().Result;это строка кода, которая никогда не должна выполняться в контексте, который вы не должны блокировать (запрос пользовательского интерфейса или ASP.NET). Даже если он не блокируется, он блокирует поток на неопределенное время. В общем, это ужасный пример. [Вам нужно выйти из потока пользовательского интерфейса перед выполнением подобного кода или использовать awaitего также, как предлагает Тони.]
ToolmakerSteve

Ответы:

82

Взгляните на этот пример , у Стивена есть для вас четкий ответ:

Вот что происходит, начиная с метода верхнего уровня ( Button1_Clickдля UI / MyController.Getдля ASP.NET):

  1. Вызов методов верхнего уровня GetJsonAsync(в контексте UI / ASP.NET).

  2. GetJsonAsyncзапускает запрос REST путем вызова HttpClient.GetStringAsync(все еще в контексте).

  3. GetStringAsyncвозвращает незавершенное Task, указывая, что запрос REST не завершен.

  4. GetJsonAsyncждет Taskвозвращенного GetStringAsync. Контекст фиксируется и будет использоваться для продолжения выполнения GetJsonAsyncметода позже. GetJsonAsyncвозвращает незавершенное Task, указывая, что GetJsonAsyncметод не завершен.

  5. Метод верхнего уровня синхронно блокируется на Taskвозвращаемых объектах GetJsonAsync. Это блокирует поток контекста.

  6. ... В конце концов, запрос REST завершится. Это завершает то, Taskчто было возвращено GetStringAsync.

  7. Продолжение для GetJsonAsyncтеперь готово к запуску и ожидает, пока контекст не станет доступным, чтобы его можно было выполнить в контексте.

  8. Тупик . Метод верхнего уровня блокирует поток контекста, ожидая GetJsonAsyncзавершения и GetJsonAsyncожидая освобождения контекста для его завершения. В примере пользовательского интерфейса «контекст» - это контекст пользовательского интерфейса; для примера ASP.NET «контекст» - это контекст запроса ASP.NET. Этот тип взаимоблокировки может быть вызван любым «контекстом».

Еще одна ссылка, которую вы должны прочитать: Await, UI и взаимоблокировки! Боже мой!

куонгле
источник
23
  • Факт 1: GetDataAsync().Result;запускается, когда задача, возвращенная функцией, GetDataAsync()завершается, в то время как он блокирует поток пользовательского интерфейса
  • Факт 2: продолжение await ( return result.ToString()) помещено в очередь для выполнения в поток пользовательского интерфейса.
  • Факт 3: задача, возвращенная пользователем, GetDataAsync()будет завершена, когда будет выполнено ее продолжение в очереди.
  • Факт 4: продолжение в очереди никогда не запускается, потому что поток пользовательского интерфейса заблокирован (Факт 1)

Тупик!

Из тупика можно выйти с помощью предоставленных альтернатив, чтобы избежать Факт 1 или Факт 2.

  • Избегайте 1,4. Вместо блокировки потока пользовательского интерфейса используйте var data = await GetDataAsync(), что позволяет потоку пользовательского интерфейса продолжать работу
  • Избегайте 2,3. Поставить в очередь продолжение await в другом var data = Task.Run(GetDataAsync).Resultнезаблокированном потоке , например use , который отправит продолжение в контекст синхронизации потока пула потоков. Это позволяет выполнить задачу, возвращенную пользователем GetDataAsync().

Это очень хорошо объясняется в статье Стивена Туба , примерно на полпути, где он использует пример DelayAsync().

Филип Нган
источник
Что касается, var data = Task.Run(GetDataAsync).Resultэто ново для меня. Я всегда думал, что внешний .Resultбудет легко доступен, как только будет запущен первый await of GetDataAsync, и так dataбудет всегда default. Интересно.
nawfal
19

Я просто снова возился с этой проблемой в проекте ASP.NET MVC. Если вы хотите вызвать asyncметоды из a PartialView, вам не разрешено создавать PartialView async. Если вы это сделаете, вы получите исключение.

Вы можете использовать следующий простой обходной путь в сценарии, когда вы хотите вызвать asyncметод из метода синхронизации:

  1. Перед звонком очистите SynchronizationContext
  2. Сделайте звонок, здесь больше не будет тупика, дождитесь его завершения
  3. Восстановить SynchronizationContext

Пример:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}
Herre Kuijpers
источник
3

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

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}
marvelTracker
источник
6
Что, если я хочу, чтобы основной поток (UI) был заблокирован до завершения задачи? Или, например, в консольном приложении? Допустим, я хочу использовать HttpClient, который поддерживает только асинхронный режим ... Как мне использовать его синхронно без риска тупиковой ситуации ? Это должно быть возможно. Если WebClient можно использовать таким образом (из-за наличия методов синхронизации) и он отлично работает, то почему нельзя этого сделать и с HttpClient?
Декстер
См. Ответ Филипа Нгана выше (я знаю, что это было опубликовано после этого комментария): Поставьте в очередь продолжение ожидания в другой незаблокированный поток, например, используйте var data = Task.Run (GetDataAsync) .Result
Jeroen
@Dexter - re « Что, если я хочу, чтобы основной поток (UI) был заблокирован до завершения задачи? » - действительно ли вы хотите, чтобы поток пользовательского интерфейса был заблокирован, то есть пользователь не может ничего делать, даже не может отменить - или это то, что вы не хотите продолжать метод, которым вы пользуетесь? "await" или "Task.ContinueWith" обрабатывают последний случай.
ToolmakerSteve
@ToolmakerSteve, конечно, я не хочу продолжать этот метод. Но я просто не могу использовать await, потому что я тоже не могу использовать async полностью - HttpClient вызывается в main , что, конечно, не может быть async. А потом я уже делал все это в приложение консоли - в этом случае я хочу точно прежний - я не хочу , чтобы мое приложение , чтобы даже быть многопоточный. Заблокируйте все .
Декстер
-1

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

Код выглядит так:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

Где метод соединения:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

Я недостаточно разбираюсь в предметной области, чтобы увидеть недостатки этого решения (если таковые имеются)

Орас
источник