HttpClient.GetAsync(...) никогда не возвращается при использовании await/async
Изменить: Этот вопрос выглядит как та же проблема, но не имеет ответов...
Изменить: В тестовом примере 5 задача, похоже, застряла в состоянии WaitingForActivation
.
Я столкнулся с некоторым нечетным поведением с использованием System.Net.Http.HttpClient в .NET 4.5 - где "ожидание" результата вызова (например,) httpClient.GetAsync(...)
никогда не вернется.
Это происходит только в определенных обстоятельствах при использовании новых функций языка асинхронного/ожидающего языков и Tasks API - код всегда работает при использовании только продолжений.
Вот некоторый код, который воспроизводит проблему - отбросьте это на новый проект MVC 4 WebApi в Visual Studio 11, чтобы открыть следующие конечные точки GET:
/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6
Каждая из конечных точек здесь возвращает те же данные (заголовки ответов из stackoverflow.com), за исключением /api/test5
, который никогда не завершается.
У меня возникла ошибка в классе HttpClient, или я неправильно использую API?
Код для воспроизведения:
public class BaseApiController : ApiController
{
/// <summary>
/// Retrieves data using continuations
/// </summary>
protected Task<string> Continuations_GetSomeDataAsync()
{
var httpClient = new HttpClient();
var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);
return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString());
}
/// <summary>
/// Retrieves data using async/await
/// </summary>
protected async Task<string> AsyncAwait_GetSomeDataAsync()
{
var httpClient = new HttpClient();
var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);
return result.Content.Headers.ToString();
}
}
public class Test1Controller : BaseApiController
{
/// <summary>
/// Handles task using Async/Await
/// </summary>
public async Task<string> Get()
{
var data = await Continuations_GetSomeDataAsync();
return data;
}
}
public class Test2Controller : BaseApiController
{
/// <summary>
/// Handles task by blocking the thread until the task completes
/// </summary>
public string Get()
{
var task = Continuations_GetSomeDataAsync();
var data = task.GetAwaiter().GetResult();
return data;
}
}
public class Test3Controller : BaseApiController
{
/// <summary>
/// Passes the task back to the controller host
/// </summary>
public Task<string> Get()
{
return Continuations_GetSomeDataAsync();
}
}
public class Test4Controller : BaseApiController
{
/// <summary>
/// Handles task using Async/Await
/// </summary>
public async Task<string> Get()
{
var data = await AsyncAwait_GetSomeDataAsync();
return data;
}
}
public class Test5Controller : BaseApiController
{
/// <summary>
/// Handles task by blocking the thread until the task completes
/// </summary>
public string Get()
{
var task = AsyncAwait_GetSomeDataAsync();
var data = task.GetAwaiter().GetResult();
return data;
}
}
public class Test6Controller : BaseApiController
{
/// <summary>
/// Passes the task back to the controller host
/// </summary>
public Task<string> Get()
{
return AsyncAwait_GetSomeDataAsync();
}
}
Ответы
Ответ 1
Вы неправильно используете API.
Здесь ситуация: в ASP.NET только один поток может обрабатывать запрос за раз. При необходимости вы можете выполнить параллельную обработку (заимствуя дополнительные потоки из пула потоков), но только один поток будет иметь контекст запроса (дополнительные потоки не имеют контекста запроса).
Это управляемый ASP.NET SynchronizationContext
.
По умолчанию, когда вы await
a Task
, метод возобновляется на захваченном SynchronizationContext
(или захваченном TaskScheduler
, если нет SynchronizationContext
). Обычно это именно то, что вы хотите: действие асинхронного контроллера будет await
что-то, и когда оно возобновится, оно возобновится с контекстом запроса.
Итак, вот почему test5
выходит из строя:
-
Test5Controller.Get
выполняет AsyncAwait_GetSomeDataAsync
(в контексте запроса ASP.NET).
-
AsyncAwait_GetSomeDataAsync
выполняет HttpClient.GetAsync
(в контексте запроса ASP.NET).
- HTTP-запрос отправляется, а
HttpClient.GetAsync
возвращает незавершенный Task
.
-
AsyncAwait_GetSomeDataAsync
ожидает Task
; поскольку он не является полным, AsyncAwait_GetSomeDataAsync
возвращает незавершенный Task
.
-
Test5Controller.Get
блокирует текущий поток до тех пор, пока Task
не завершится.
- Входит HTTP-ответ, и
Task
, возвращаемый HttpClient.GetAsync
, завершен.
-
AsyncAwait_GetSomeDataAsync
пытается возобновиться в контексте запроса ASP.NET. Однако в этом контексте уже есть поток: поток заблокирован в Test5Controller.Get
.
- Тупик.
Вот почему работают другие:
- (
test1
, test2
и test3
): Continuations_GetSomeDataAsync
планирует продолжение в пуле потоков за пределами контекста запроса ASP.NET. Это позволяет завершить Task
, возвращаемый Continuations_GetSomeDataAsync
, без повторного ввода контекста запроса.
- (
test4
и test6
): Поскольку ожидается Task
, поток запросов ASP.NET не заблокирован. Это позволяет AsyncAwait_GetSomeDataAsync
использовать контекст запроса ASP.NET, когда он готов к продолжению.
И здесь лучшие практики:
- В методах
async
используйте ConfigureAwait(false)
, когда это возможно. В вашем случае это изменит AsyncAwait_GetSomeDataAsync
на var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
- Не блокировать
Task
s; он async
полностью вниз. Другими словами, использовать await
вместо GetResult
(Task.Result
и Task.Wait
также следует заменить на await
).
Таким образом, вы получаете оба преимущества: продолжение (оставшаяся часть метода AsyncAwait_GetSomeDataAsync
) выполняется в потоке пула основных потоков, который не должен вводить контекст запроса ASP.NET; и сам контроллер async
(который не блокирует поток запроса).
Дополнительная информация:
- My
async
/await
intro post, который включает краткое описание того, как Task
awaiters используют SynchronizationContext
.
- Часто задаваемые вопросы по асинхронному использованию/оживанию, в котором более подробно рассматриваются контексты. Также см. Ожидание, и пользовательский интерфейс, и взаимоблокировки! О, мой!, который применяется здесь, даже если вы находитесь в ASP.NET, а не в пользовательском интерфейсе, потому что ASP.NET
SynchronizationContext
ограничивает контекст запроса только одним потоком за раз.
- Это сообщение форума MSDN.
- Stephen Toub demos этот тупик (с использованием пользовательского интерфейса), и так делает Lucian Wischik.
Обновление 2012-07-13: Включено этот ответ в сообщение в блоге.
Ответ 2
Изменение: как правило, старайтесь избегать выполнения ниже, кроме как последнее усилие рва, чтобы избежать тупиков. Прочитайте первый комментарий от Стивена Клири.
Быстрое решение здесь. Вместо того чтобы писать:
Task tsk = AsyncOperation();
tsk.Wait();
Пытаться:
Task.Run(() => AsyncOperation()).Wait();
Или если вам нужен результат:
var result = Task.Run(() => AsyncOperation()).Result;
Из исходного кода (отредактировано в соответствии с приведенным выше примером):
AsyncOperation теперь будет вызываться в ThreadPool, где не будет SynchronizationContext, и продолжения, используемые внутри AsyncOperation, не будут принудительно возвращаться в вызывающий поток.
Для меня это выглядит как полезный вариант, так как у меня нет возможности сделать его асинхронным полностью (что я бы предпочел).
Из источника:
Убедитесь, что ожидание в методе FooAsync не находит контекст, к которому нужно вернуться. Самый простой способ сделать это - вызвать асинхронную работу из ThreadPool, например, обернуть вызов в Task.Run, например
int Sync() {return Task.Run(() => Library.FooAsync()). Result; } }
FooAsync теперь будет вызываться в ThreadPool, где не будет SynchronizationContext, и продолжения, используемые внутри FooAsync, не будут принудительно возвращаться в поток, который вызывает Sync().
Ответ 3
Так как вы используете .Result
или .Wait
или await
это приведет к тупику в вашем коде.
вы можете использовать ConfigureAwait(false)
в async
методах для предотвращения взаимоблокировки
как это:
var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false);
вы можете использовать ConfigureAwait(false)
везде, где это возможно, для "Не блокировать асинхронный код".
Ответ 4
Эти две школы действительно не исключают.
Вот сценарий, в котором вам просто нужно использовать
Task.Run(() => AsyncOperation()).Wait();
или что-то вроде
AsyncContext.Run(AsyncOperation);
У меня есть действие MVC, которое связано с атрибутом транзакции базы данных. Идея была (вероятно) отбросить все, что было сделано в действии, если что-то пойдет не так. Это не позволяет переключать контекст, в противном случае откат транзакции или фиксация будет неудачной.
Мне нужна библиотека async, так как ожидается, что она будет работать async.
Единственный вариант. Запустите его как обычный синхронизирующий вызов.
Я просто говорю каждому свое.
Ответ 5
Я ищу здесь:
http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx
И здесь:
http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx
И видя:
Этот тип и его члены предназначены для использования компилятором.
Учитывая, что версия await
работает и является "правильным" способом делать вещи, вам действительно нужен ответ на этот вопрос?
Мое голосование: Неправильное использование API.
Ответ 6
Я собираюсь поместить это здесь больше для полноты, чем прямого отношения к ФП. Я потратил почти день на отладку запроса HttpClient
, удивляясь, почему я так и не получил ответ.
Наконец обнаружил, что я забыл await
async
вызова дальше по стеку вызовов.
Ощущает себя так же хорошо, как пропустить точку с запятой.