HttpClient.GetAsync (…) никогда не возвращается при использовании await / async

315

Изменить: Этот вопрос выглядит, как будто это может быть та же проблема, но не имеет ответов ...

Изменить: в тестовом примере 5 задача застряла в WaitingForActivationсостоянии.

Я столкнулся с каким-то странным поведением при использовании System.Net.Http.HttpClient в .NET 4.5, где «ожидание» результата вызова (например) httpClient.GetAsync(...)никогда не вернется.

Это происходит только при определенных обстоятельствах при использовании новых функций языка async / await и 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();
    }
}
Бенджамин Фокс
источник
2
Похоже, это не та же проблема, но просто чтобы убедиться, что вы знаете об этом, есть ошибка MVC4 в асинхронных методах бета-тестирования WRT, которые завершаются синхронно - см. Stackoverflow.com/questions/9627329/…
Джеймс Мэннинг,
Спасибо - я буду следить за этим. В этом случае я думаю, что метод всегда должен быть асинхронным из-за вызова HttpClient.GetAsync(...)?
Бенджамин Фокс

Ответы:

468

Вы неправильно используете API.

Вот ситуация: в ASP.NET только один поток может обрабатывать запрос одновременно. При необходимости можно выполнить некоторую параллельную обработку (заимствование дополнительных потоков из пула потоков), но только один поток будет иметь контекст запроса (дополнительные потоки не имеют контекста запроса).

Это управляется ASP.NETSynchronizationContext .

По умолчанию, когда вы , метод возобновляется на захваченный (или не захвачен , если нет ). Обычно это как раз то, что вы хотите: асинхронное действие контроллера будет чем-то, и когда оно возобновится, оно возобновится с контекстом запроса.awaitTaskSynchronizationContextTaskSchedulerSynchronizationContextawait

Итак, вот почему 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, когда он готов продолжить.

И вот лучшие практики:

  1. В ваших «библиотечных» asyncметодах используйте по ConfigureAwait(false)возможности. В вашем случае это изменится AsyncAwait_GetSomeDataAsyncнаvar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Не блокируйте на Taskс; это asyncвсе вниз. Другими словами, используйте awaitвместо GetResult( Task.Resultи Task.Waitтакже следует заменить на await).

Таким образом, вы получаете оба преимущества: продолжение (оставшаяся часть AsyncAwait_GetSomeDataAsyncметода) запускается в базовом потоке пула потоков, который не должен входить в контекст запроса ASP.NET; и сам контроллер async(который не блокирует поток запроса).

Больше информации:

Обновление 2012-07-13: Включите этот ответ в сообщение в блоге .

Стивен Клири
источник
2
Есть ли какая-то документация для ASP.NET SynchroniztaionContext, объясняющая, что для какого-то запроса в контексте может быть только один поток? Если нет, я думаю, что должно быть.
свик
8
Это нигде не задокументировано AFAIK.
Стивен Клири
10
Спасибо - отличный ответ. Разница в поведении (по-видимому) функционально идентичного кода расстраивает, но имеет смысл с вашим объяснением. Было бы полезно, если бы фреймворк мог обнаруживать такие тупики и вызывать где-то исключение.
Бенджамин Фокс
3
Есть ситуации, когда использование .ConfigureAwait (false) в контексте asp.net НЕ рекомендуется? Мне кажется, что его всегда следует использовать и что его следует использовать только в контексте пользовательского интерфейса, поскольку его не нужно использовать, поскольку вам необходимо синхронизировать его с пользовательским интерфейсом. Или я упускаю суть?
AlexGad
3
ASP.NET SynchronizationContextпредоставляет некоторые важные функции: он передает контекст запроса. Это включает в себя все виды вещей от аутентификации до куки-файлов и культуры. Таким образом, в ASP.NET вместо синхронизации с пользовательским интерфейсом выполняется синхронизация с контекстом запроса. Это может измениться в ближайшее время: у нового ApiControllerдействительно есть HttpRequestMessageконтекст как свойство - таким образом, может не потребоваться проходить контекст через SynchronizationContext- но я еще не знаю.
Стивен Клири
62

Изменить: как правило, старайтесь избегать выполнения ниже, кроме как последнее усилие рва, чтобы избежать тупиков. Прочитайте первый комментарий от Стивена Клири.

Быстрое решение здесь . Вместо того чтобы писать:

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 ().

Ykok
источник
7
Возможно, хотите перечитать ссылку на источник; Автор рекомендует не делать этого. Это работает? Да, но только в том смысле, что вы избежите тупика. Это решение сводит на нет все преимущества asyncкода в ASP.NET и фактически может вызвать проблемы в масштабе. Кстати, ConfigureAwaitне нарушает правильное асинхронное поведение в любом сценарии; это именно то, что вы должны использовать в коде библиотеки.
Стивен Клири
2
Это весь первый раздел, выделенный жирным шрифтом Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. Вся остальная часть поста объясняет несколько разных способов сделать это, если вам абсолютно необходимо .
Стивен Клири
1
Добавил раздел, который я нашел в источнике - я оставлю это на усмотрение будущих читателей. Обратите внимание, что вы, как правило, должны стараться избегать этого и делать это только в качестве крайней меры (т. Е. При использовании асинхронного кода, который вы не можете контролировать).
Йок
3
Мне нравятся все ответы здесь и как всегда .... все они основаны на контексте (каламбур намеревается lol). Я обертываю асинхронные вызовы HttpClient с синхронной версией, поэтому я не могу изменить этот код, чтобы добавить ConfigureAwait в эту библиотеку. Таким образом, чтобы предотвратить взаимные блокировки в производственной среде, я обертываю асинхронные вызовы в Task.Run. Итак, насколько я понимаю, это будет использовать 1 дополнительный поток на запрос и избежать тупика. Я предполагаю, что для полного соответствия мне нужно использовать методы синхронизации WebClient. Это большая работа, чтобы оправдать себя, поэтому мне понадобится веская причина не придерживаться моего нынешнего подхода.
Самнерский
1
В итоге я создал метод расширения для преобразования Async в Sync. Я где-то читал здесь то же самое, что и .Net Framework делает это: public static TResult RunSync <TResult> (this Func <Task <TResult >> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter () .GetResult (); }
Самнерик
10

Поскольку вы используете .Resultили, .Waitили awaitэто приведет к тупику в вашем коде.

Вы можете использовать ConfigureAwait(false)в asyncметодах предотвращения тупика

как это:

var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead)
                             .ConfigureAwait(false);

Вы можете использовать, ConfigureAwait(false)где это возможно, для Не блокировать асинхронный код.

Хасан Фатхи
источник
2

Эти две школы на самом деле не исключают.

Вот сценарий, где вы просто должны использовать

   Task.Run(() => AsyncOperation()).Wait(); 

или что-то вроде

   AsyncContext.Run(AsyncOperation);

У меня есть действие MVC, которое находится под атрибутом транзакции базы данных. Идея была (вероятно) откатить все, что было сделано в действии, если что-то пойдет не так. Это не позволяет переключение контекста, в противном случае откат транзакции или фиксация само по себе приведет к сбою.

Мне нужна библиотека async, так как ожидается, что она будет работать async.

Единственный вариант. Запустите его как обычный вызов синхронизации.

Я просто говорю каждому свое.

alex.peter
источник
так ты предлагаешь первый вариант в своем ответе?
Дон Чидл
1

Я собираюсь поместить это здесь больше для полноты, чем для непосредственного отношения к ФП. Я потратил почти день на отладку HttpClientзапроса, задаваясь вопросом, почему я так и не получил ответ.

Наконец обнаружил, что я забыл awaitо asyncвызове дальше вниз по стеку вызовов.

Ощущает себя так же хорошо, как пропустить точку с запятой.

Bondolin
источник
-1

Я смотрю здесь:

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 .

Ямен
источник
Я не заметил этого, хотя я видел другой язык, который указывает, что использование API GetResult () является поддерживаемым (и ожидаемым) вариантом использования.
Бенджамин Фокс
1
В дополнение к этому, если вы Test5Controller.Get()выполните рефакторинг для устранения ожидающего со следующим: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;То же поведение можно наблюдать.
Бенджамин Фокс