Пример async/wait, который вызывает тупик

Я столкнулся с некоторыми передовыми методами асинхронного программирования с использованием ключевых слов async/await С# (я новичок в С# 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(). Результат", Между тем, скажем, данные готовы. Почему основной поток не продолжает эту логику продолжения и возвращает результат строки из GetDataAsync()?

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

Ответ 1

Посмотрите пример в здесь, у Стивена есть ясный ответ для вас:

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

  • Метод верхнего уровня вызывает GetJsonAsync (в контексте UI/ASP.NET).

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

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

  • GetJsonAsync ожидает задачу, возвращенную GetStringAsync. Контекст захвачен и будет использоваться для продолжения работы Метод GetJsonAsync позже. GetJsonAsync возвращает незавершенную задачу, что метод GetJsonAsync не завершен.

  • Метод верхнего уровня синхронно блокирует задачу, возвращенную GetJsonAsync. Это блокирует контекстный поток.

  • ... В конце концов, запрос REST завершится. Это завершает задачу, возвращенную GetStringAsync.

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

  • Тупик

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

Еще одна ссылка, которую вы должны прочитать:

Ожидание, и пользовательский интерфейс, и взаимоблокировки! О, мой!

Ответ 2

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

Тупик!

Тупик может быть нарушен предоставленными альтернативами, чтобы избежать Факт 1 или Факт 2.

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

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

Ответ 3

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

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();
}

Ответ 4

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

Таким образом, в основном простой способ в сценарии, где вы хотите вызвать метод 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);
    }

Ответ 5

Произведение вокруг я пришел к тому, чтобы использовать 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);
        }
    }
}

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