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


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

Edit: в тестовом случае 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();
    }
}
5 263

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. не блокировать Task s; это async полностью вниз. Другими словами, используйте await вместо GetResult (Task.Result и Task.Wait также следует заменить await).

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

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

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

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

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

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

попробуй:

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

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

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

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

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

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

источник

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

int Sync() { возвращение задачи.Запустить(() => Библиотека.FooAsync()).Результат; }

FooAsync теперь будет вызван на ThreadPool, где не будет a 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, которое находится под атрибутом транзакции базы данных. Идея была (вероятно) откатить все, что сделано в действии, если что-то пойдет не так. Это не позволяет переключать контекст, в противном случае откат транзакции или фиксация завершится неудачей.

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

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

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

Я смотрю тут:

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-интерфейс.