HttpClient.GetAsync (…) ніколи не повертається під час використання функції wait / async


315

Edit: Це питання буде схожий на це може бути та ж проблема, але не має жодних відповідей ...

Редагувати: У тестовому випадку 5 завдання, здається, застрягло у WaitingForActivationстані.

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

Це трапляється лише за певних обставин, коли використовується нова функціональність мови асинхронізації / очікування і 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 .

За замовчуванням, коли ви awaita 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, коли він готовий до продовження.

Ось найкращі практики:

  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: Включіть цю відповідь у публікацію в блозі .


2
Чи є якась документація для ASP.NET, SynchroniztaionContextяка пояснює, що в контексті для якогось запиту може бути лише одна нитка? Якщо ні, я думаю, що повинно бути.
svick

8
Це ніде не зафіксовано AFAIK.
Стівен Клірі

10
Спасибі - чудова відповідь. Різниця в поведінці між (мабуть) функціонально однаковим кодом засмучує, але має сенс у вашому поясненні. Було б корисно, якби рамки змогли виявити такі тупики та десь створити виняток.
Бенджамін Фокс

3
Чи бувають ситуації, коли використання .ConfigureAwait (false) в контексті asp.net НЕ рекомендується? Мені здається, що його завжди слід використовувати і що це лише в контексті інтерфейсу, що він не повинен використовуватися, оскільки потрібно синхронізувати його з інтерфейсом. Або я пропускаю суть?
AlexGad

3
ASP.NET SynchronizationContextнадає важливу функціональність: він передає контекст запиту. Сюди входять усі види матеріалів - від автентифікації до файлів cookie до культури. Отже, в ASP.NET замість синхронізації з інтерфейсом ви синхронізуєте назад до контексту запиту. Це може змінитися незабаром: новий ApiControllerмає HttpRequestMessageконтекст як властивість - тому, можливо, не потрібно буде протікати контекст SynchronizationContext- але я ще не знаю.
Стівен Клірі

61

Редагувати: Як правило, намагайтеся уникати наведених нижче дій, за винятком останніх зусиль, щоб уникнути тупиків. Прочитайте перший коментар від Стівена Клірі.

Швидке виправлення звідси . Замість написання:

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 ()). Результат; }

FooAsync тепер буде викликатись у ThreadPool, де не буде SynchronizationContext, і продовження, яке використовується всередині FooAsync, не буде повернено до потоку, що викликає Sync ().


7
Ви можете перечитати своє посилання на джерело; автор рекомендує цього не робити. Це працює? Так, але лише в тому сенсі, що ви уникаєте тупикової ситуації. Це рішення заперечує всі переваги asyncкоду на ASP.NET, і насправді може спричинити проблеми в масштабі. BTW, ConfigureAwaitне "порушує належну асинхронну поведінку" в жодному сценарії; це саме те, що ви повинні використовувати в коді бібліотеки.
Стівен Клірі

2
Це весь перший розділ, що має жирний шрифт Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. Увесь решту публікації пояснює декілька різних способів зробити це, якщо вам абсолютно потрібно .
Стівен Клірі

1
Доданий розділ, який я знайшов у джерелі - я залишаю його для вирішення майбутніх читачів. Зауважте, що ви, як правило, намагайтеся уникати цього і робити це лише як останній курорт канави (тобто, використовуючи код асинхронізації, ви не маєте контролю над цим).
Ykok

3
Мені подобаються всі відповіді тут і як завжди .... всі вони базуються на контексті (каламбур призначений хаха). Я завершую виклики Async HttpClient із синхронною версією, тому не можу змінити цей код, щоб додати ConfigureAwait до цієї бібліотеки. Щоб запобігти тупиковість у виробництві, я загортаю виклики Async у Task.Run. Оскільки я розумію, це використовуватиме 1 додатковий потік на запит і уникатиме тупикової ситуації. Я припускаю, що щоб бути повністю сумісним, мені потрібно використовувати методи синхронізації WebClient. Це дуже багато роботи для виправдання, тому мені знадобиться переконлива причина, щоб не дотримуватися мого нинішнього підходу.
самнерік

1
Я в кінцевому підсумку створив метод розширення для перетворення Async в Sync. Я читаю тут десь так само, як це робить .Net Framework: публічний статичний TResult RunSync <TResult> (цей Func <Завдання <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, оскільки очікується запуск асинхронізації.

Єдиний варіант. Запустити його як звичайний виклик синхронізації.

Я просто кажу кожному своєму.


тож ви пропонуєте перший варіант у своїй відповіді?
Дон Чедл

1

Я збираюся викласти це тут більше для повноти, ніж безпосереднього відношення до ОП. Я витратив майже день налагодження HttpClientзапиту, цікавившись, чому я ніколи не отримував відповіді.

Нарешті знайшов , що я забув awaitпро asyncвиклик подальшого вниз стека викликів.

Відчуває себе так само добре, як і пропустити крапку з комою.


-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;Таку ж поведінку можна спостерігати.
Бенджамін Фокс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.