Пример 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 из метода синхронизации, вы можете сделать следующее:
- перед вызовом очистить SynchronizationContext
- сделайте вызов, здесь больше не будет тупика, дождитесь его завершения
- восстановить 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);
}
}
}
Я недостаточно в этом домене, чтобы увидеть недостатки этого решения (если есть)