it-roy-ru.com

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

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

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

79
Dror Weiss

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

Вот что происходит, начиная с метода верхнего уровня (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. Этот тип тупика может быть вызван для либо «контекст».

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

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

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

Тупик!

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

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

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

11
Phillip Ngan

Я просто возился с этой проблемой снова в проекте 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);
    }
10
Herre Kuijpers

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

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

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

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

0
Orace