Вопрос по asynchronous, .net, async-await, c# – HttpClient.GetAsync (…) никогда не возвращается при использовании await / async

279

Редактировать Этот вопро похоже, что это может быть та же проблема, но не имеет ответов ...

Редактировать В тестовом примере 5 задача застряла вWaitingForActivation штат

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

Это происходит только при определенных обстоятельствах, когда используются новые функции языка async / await и API Tasks - кажется, что код всегда работает при использовании только продолжений.

Вот некоторый код, который воспроизводит проблему - поместите его в новый «проект 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();
    }
}
Похоже, это не та проблема, но просто чтобы убедиться, что вы знаете об этом, есть ошибка MVC4 в асинхронных методах бета-тестирования WRT, которые выполняются синхронно - см. / Stackoverflow.com вопросы / 9627329 / ... James Manning
Спасибо - я буду следить за этим. В этом случае я думаю, что метод всегда должен быть асинхронным из-за вызоваHttpClient.GetAsync(...)? Benjamin Fox

Ваш Ответ

5   ответов
421

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

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

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

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

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

Мояasync/await вступительное сообщение, который включает краткое описание того, какTask официанты используютSynchronizationContext.The Async / Await FAQ, которая более подробно описывает контекст. Также см Жду, и пользовательский интерфейс, и тупики! О, мой! который Делает применять здесь, даже если вы находитесь в ASP.NET, а не в пользовательском интерфейсе, потому что ASP.NETSynchronizationContext ограничивает контекст запроса только одним потоком за раз.Это MSDN сообщение на форуме. Стефан Туб удалить этот тупик (используя пользовательский интерфейс), а также Лусиан Висчик то.

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

Это нигде не задокументировано AFAIK. Stephen Cleary
Есть ли какая-то документация для ASP.NETSynchroniztaionContext это объясняет, что для какого-то запроса может быть только один поток в контексте? Если нет, я думаю, что должно быть. svick
Существуют ли ситуации, когда использование .ConfigureAwait (false) в контексте asp.net НЕ рекомендуется? Мне кажется, что он всегда должен использоваться и только в контексте пользовательского интерфейса его не следует использовать, поскольку вам нужно синхронизировать его с пользовательским интерфейсом. Или я упускаю суть? AlexGad
Благодарность -здоров ответ. Разница в поведении (по-видимому) функционально идентичного кода расстраивает, но имеет смысл с вашим объяснением. Было бы полезно, если бы фреймворк мог обнаруживать такие тупики и вызывать где-то исключение. Benjamin Fox
ASP.NETSynchronizationContext предоставляет некоторые важные функции: он передает контекст запроса. Это включает в себя все виды вещей от аутентификации до куки-файлов и культуры. Таким образом, в ASP.NET вместо синхронизации с пользовательским интерфейсом выполняется синхронизация с контекстом запроса. Это может измениться в ближайшее время: новыйApiController имеетHttpRequestMessage контекст как свойство - так оно и естьможе не требуется пропускать контекст черезSynchronizationContext - но я пока не знаю. Stephen Cleary
54

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

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

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

Могу перечитать ссылку на источник; автор рекомендуетн делая это. Это работает? Да, но только в том смысле, что вы избежите тупика. Это решение отрицаетвс преимуществаasync код на ASP.NET, и на самом деле может вызвать проблемы в масштабе. Кстати,ConfigureAwait не нарушает правильное асинхронное поведение в любом сценарии; это именно то, что вы должны использовать в коде библиотеки. Stephen Cleary
Я перечитал ссылку на источник. Не удалось найти то, что вы имели в виду. Но если глава «Пример из реального мира» неверно прочитана, может показаться, что он рекомендует не делать этого, но на самом деле он рассматривает совершенно другой случа Ykok
Это первый раздел, выделенный жирным шрифтомAvoid Exposing Synchronous Wrappers for Asynchronous Implementations. Вся остальная часть поста объясняет несколько разных способов сделать этесл ты абсолютнонужн к. Stephen Cleary
Добавил раздел, который я нашел в источнике - я оставлю это на усмотрение будущих читателей. Обратите внимание, что вы, как правило, должны стараться избегать этого и делать это только в качестве крайней меры (т. Е. При использовании асинхронного кода, который вы не можете контролировать). Ykok
Мне нравятся все ответы здесь и, как всегда, все они основаны на контексте (каламбур). Я обертываю асинхронные вызовы HttpClient с синхронной версией, поэтому я не могу изменить этот код, чтобы добавить ConfigureAwait в эту библиотеку. Таким образом, чтобы предотвратить взаимные блокировки в производственной среде, я обертываю асинхронные вызовы в Task.Run. Итак, насколько я понимаю, это будет использовать 1 дополнительный поток на запрос и избежать тупика. Я предполагаю, что для полного соответствия мне нужно использовать методы синхронизации WebClient. Это большая работа, чтобы оправдать себя, поэтому мне понадобится веская причина не придерживаться моего нынешнего подхода. samneric
4

.Result или.Wait илиawait это в конечном итоге приведет к Тупиковый в вашем коде.

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

так

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

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

1

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

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

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

   AsyncContext.Run(AsyncOperation);

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

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

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

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

так ты предлагаешь первый вариант ответа? mmcrae
-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.

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

Похожие вопросы