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 (который не блокирует поток запроса).

Дополнительная информация:

Обновление 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 вызова дальше по стеку вызовов.

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