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

Edit: этот вопрос похоже, это может быть та же проблема, но нет ответов...

Edit: в тестовом случае 5 Задача, похоже, застряла в WaitingForActivation государство.

я столкнулся с некоторым странным поведением, используя систему.Сеть.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();
    }
}

5 ответов


вы злоупотребляете 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 возвращено это.
  • AsyncAwait_GetSomeDataAsync попытки возобновить внутри ASP.NET контекст запроса. Тем не менее, уже есть поток в этом контексте: поток заблокирован в Test5Controller.Get.
  • тупик.

вот почему работают другие:

  • (test1, test2 и test3): Continuations_GetSomeDataAsync планирует продолжение в пул потоков,за пределами ASP.NET контекст запроса. Это позволяет Task возвращено Continuations_GetSomeDataAsync для завершения без необходимости повторного ввода контекста запроса.
  • (test4 и test6): С Task is ждал, 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. не блокировать на Tasks; это async полностью вниз. Другими словами, используйте await вместо GetResult (Task.Result и Task.Wait также следует заменить await).

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

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

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


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

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

Task tsk = AsyncOperation();
tsk.Wait();

попробуй:

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

или, если вам нужен результат:

var result = Task.Run(() => AsyncOperation()).Result;

из источника (отредактировано в соответствии с приведенным выше примером):

AsyncOperation теперь будет вызываться в ThreadPool, где есть не будет SynchronizationContext и продолжения, используемые внутри AsyncOperation не будет принудительно возвращен к вызывающему потоку.

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

источник

убедитесь, что await в методе FooAsync не находит контекста для маршал вернулся. Самый простой способ сделать это-вызвать асинхронная работа с ThreadPool, как путем оборачивать вызов в задаче.Запустить, например,

синхронизация int() { задача возврата.Выполнить (() = > Библиотека.FooAsync()).Результат; }

FooAsync теперь будет вызываться в ThreadPool, где не будет SynchronizationContext и продолжения, используемые внутри FooAsync не будет принудительно возвращен в поток, вызывающий Sync ().


если вы используете .Result или .Wait или await это в конечном итоге вызовет тупик в коде.

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

такой:

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

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


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

вот сценарий, в котором вам просто нужно использовать

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

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

   AsyncContext.Run(AsyncOperation);

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

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

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

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


Я смотрю тут:

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.