Пример асинхронного ожидания / ожидания, который вызывает тупик

Я натолкнулся на некоторые лучшие практики для асинхронного программирования с использованием C# async/await ключевые слова (я новичок в 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()?

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

5 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

Жду и UI, и тупиков! Боже мой!

  • Факт 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, который опубликует продолжение в контексте синхронизации потока пула. Это позволяет задачу, возвращаемую GetDataAsync() завершить.

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

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

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

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

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

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

Работа, к которой я пришел, заключается в использовании 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);
        }
    }
}

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

Другие вопросы по тегам