Рекомендуется вызывать ConfigureAwait для всего кода на стороне сервера

562

Когда у вас есть серверный код (то есть какой-то ApiController), и ваши функции асинхронны - поэтому они возвращаются Task<SomeObject>- считается ли наилучшей практикой всякий раз, когда вы ожидаете функции, которые вы вызываете ConfigureAwait(false)?

Я читал, что он более производительный, так как он не должен переключать контексты потока обратно в исходный контекст потока. Однако в ASP.NET Web Api, если ваш запрос поступает в один поток, и вы ожидаете какую-то функцию и вызов, ConfigureAwait(false)которые потенциально могут перевести вас в другой поток, когда вы возвращаете окончательный результат своей ApiControllerфункции.

Я напечатал пример того, о чем я говорю ниже:

public class CustomerController : ApiController
{
    public async Task<Customer> Get(int id)
    {
        // you are on a particular thread here
        var customer = await SomeAsyncFunctionThatGetsCustomer(id).ConfigureAwait(false);

        // now you are on a different thread!  will that cause problems?
        return customer;
    }
}
Араш Эмами
источник

Ответы:

629

Обновление: ASP.NET Core не имеетSynchronizationContext . Если вы используете ASP.NET Core, не имеет значения, используете вы его ConfigureAwait(false)или нет.

Для ASP.NET "Полный" или "Классический" или что-то еще, остальная часть этого ответа все еще применяется.

Оригинальный пост (для не Core ASP.NET):

Это видео от команды ASP.NET содержит лучшую информацию об использовании asyncв ASP.NET.

Я читал, что он более производительный, так как он не должен переключать контексты потока обратно в исходный контекст потока.

Это верно для приложений пользовательского интерфейса, где есть только один поток пользовательского интерфейса, с которым вы должны «синхронизироваться» обратно.

В ASP.NET ситуация немного сложнее. Когда asyncметод возобновляет выполнение, он получает поток из пула потоков ASP.NET. Если вы отключите захват контекста с помощью ConfigureAwait(false), тогда поток просто продолжит выполнение метода напрямую. Если вы не отключите захват контекста, поток повторно введет контекст запроса и продолжит выполнение метода.

Так ConfigureAwait(false)что не спасает вас от скачка потока в ASP.NET; это спасает вас от повторного ввода контекста запроса, но обычно это происходит очень быстро. ConfigureAwait(false) может быть полезно, если вы пытаетесь выполнить небольшую параллельную обработку запроса, но на самом деле TPL лучше подходит для большинства этих сценариев.

Однако в ASP.NET Web Api, если ваш запрос поступает в один поток, и вы ожидаете какую-то функцию и вызываете ConfigureAwait (false), которая потенциально может перевести вас в другой поток, когда вы возвращаете конечный результат вашей функции ApiController ,

На самом деле, просто сделать это awaitможет сделать это. Как только ваш asyncметод достигает await, метод блокируется, но поток возвращается в пул потоков. Когда метод готов продолжить, любой поток вырывается из пула потоков и используется для возобновления метода.

Единственное отличие ConfigureAwaitв ASP.NET заключается в том, входит ли этот поток в контекст запроса при возобновлении метода.

У меня есть дополнительная справочная информация в моей статье на MSDNSynchronizationContext и в моем asyncвступительном блоге .

Стивен Клири
источник
23
Локальное хранилище потока не передается никаким контекстом. HttpContext.Currentпередается ASP.NET SynchronizationContext, который по умолчанию передается вам await, но не передается ContinueWith. Ото, контекст исполнения ( в том числе ограничений безопасности) контекст упоминается в CLR с помощью C #, и это текло оба ContinueWithи await(даже если вы используете ConfigureAwait(false)).
Стивен Клири
65
Не было бы замечательно, если бы C # имел поддержку родного языка для ConfigureAwait (false)? Что-то вроде 'awaitnc' (не ожидайте контекста). Распечатка отдельного вызова метода везде раздражает. :)
NathanAldenSr
19
@NathanAldenSr: это обсуждалось совсем немного. Проблема с новым ключевым словом заключается в том, что на ConfigureAwaitсамом деле имеет смысл только когда вы ожидаете задачи , тогда как awaitдействует на любое «ожидаемое». Рассматривались и другие варианты: должно ли поведение по умолчанию отбрасывать контекст в библиотеке? Или есть настройка компилятора для поведения контекста по умолчанию? Оба они были отклонены, потому что сложнее просто прочитать код и рассказать, что он делает.
Стивен Клири
10
@AnshulNigam: Вот почему действия контроллера нуждаются в их контексте. Но большинство методов, вызываемых действиями, этого не делают.
Стивен Клири
14
@JonathanRoeder: Вообще говоря, вам не нужно ConfigureAwait(false)избегать тупиков на основе Result/, Waitпотому что в ASP.NET вы не должны использовать Result/ Waitв первую очередь.
Стивен Клири
131

Краткий ответ на ваш вопрос: Нет. Вы не должны звонить ConfigureAwait(false)на уровне приложения, как это.

TL; DR-версия длинного ответа: если вы пишете библиотеку, в которой вы не знаете своего потребителя и не нуждаетесь в контексте синхронизации (чего не следует делать в библиотеке, я полагаю), вы должны всегда использовать ConfigureAwait(false). В противном случае потребители вашей библиотеки могут столкнуться с тупиковыми ситуациями при использовании асинхронных методов в режиме блокировки. Это зависит от ситуации.

Вот немного более подробное объяснение важности ConfigureAwaitметода (цитата из моего поста в блоге):

Когда вы ожидаете метода с ключевым словом await, компилятор генерирует кучу кода от вашего имени. Одной из целей этого действия является обработка синхронизации с потоком пользовательского интерфейса (или основного потока). Ключевым компонентом этой функции является тот, SynchronizationContext.Currentкоторый получает контекст синхронизации для текущего потока. SynchronizationContext.Currentзаполняется в зависимости от среды, в которой вы находитесь. GetAwaiterМетод Task ищет SynchronizationContext.Current. Если текущий контекст синхронизации не равен нулю, продолжение, переданное этому ожидающему, будет отправлено обратно в этот контекст синхронизации.

При использовании метода, который использует новые функции асинхронного языка, в режиме блокировки, вы получите тупик, если у вас есть доступный SynchronizationContext. Когда вы используете такие методы в режиме блокировки (ожидание в методе Task с методом Wait или получение результата непосредственно из свойства Result задачи), вы одновременно заблокируете основной поток. Когда в конечном итоге Задача завершается внутри этого метода в пуле потоков, она будет вызывать продолжение для отправки обратно в основной поток, потому что SynchronizationContext.Currentоно доступно и захвачено. Но здесь есть проблема: поток пользовательского интерфейса заблокирован, и у вас тупик!

Кроме того, вот две отличные статьи для вас, которые именно для вашего вопроса:

Наконец, есть отличное короткое видео от Lucian Wischik , посвященное именно этой теме: методы асинхронной библиотеки должны учитывать использование Task.ConfigureAwait (false) .

Надеюсь это поможет.

tugberk
источник
2
«Метод GetAwaiter Task выполняет поиск SynchronizationContext.Current. Если текущий контекст синхронизации не равен нулю, продолжение, переданное этому ожидающему, будет отправлено обратно в этот контекст синхронизации». - У меня Taskскладывается впечатление, что вы пытаетесь сказать, что идет по стеку, чтобы получить то SynchronizationContext, что неправильно. Значение SynchronizationContextперед захватом передается, Taskа затем остальная часть кода продолжается, SynchronizationContextесли значение SynchronizationContext.Currentне равно нулю.
casperOne
1
@casperOne Я хотел сказать то же самое.
Тагберк
8
Разве это не обязанность вызывающего абонента гарантировать, что SynchronizationContext.Currentвсе ясно, или что библиотека вызывается внутри, Task.Run()а не записывается по .ConfigureAwait(false)всей библиотеке классов?
Бинки
1
@binki - с другой стороны: (1) предположительно, библиотека используется во многих приложениях, поэтому единовременное использование библиотеки для облегчения работы приложений является экономически эффективным; (2) предположительно, автор библиотеки знает, что он написал код, который не имеет оснований требовать продолжения в исходном контексте, который он выражает с помощью тех .ConfigureAwait(false). Возможно, авторам библиотек было бы легче, если бы это было поведение по умолчанию, но я бы предположил, что сделать правильную работу библиотеки немного сложнее, чем усложнить правильное написание приложения.
ToolmakerSteve
4
Почему автор библиотеки должен поддерживать пользователя? Если потребитель хочет зайти в тупик, почему я должен помешать им?
Quarkly
26

Самый большой недостаток, который я обнаружил при использовании ConfigureAwait (false), заключается в том, что культура потоков возвращается к системному значению по умолчанию. Если вы настроили культуру, например ...

<system.web>
    <globalization culture="en-AU" uiCulture="en-AU" />    
    ...

и вы размещаете на сервере, для которого установлена ​​культура en-US, тогда вы обнаружите, что ConfigureAwait (false) будет называться CultureInfo.CurrentCulture вернет en-AU и после того, как вы получите en-US. т.е.

// CultureInfo.CurrentCulture ~ {en-AU}
await xxxx.ConfigureAwait(false);
// CultureInfo.CurrentCulture ~ {en-US}

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

Мик
источник
28
Современные версии .NET (я думаю, начиная с 4.6?) Будут распространять культуру между потоками, даже если ConfigureAwait(false)она используется.
Стивен Клири
1
Спасибо за информацию. Мы действительно используем .net 4.5.2
Мик
11

У меня есть некоторые общие мысли о реализации Task:

  1. Задача одноразовая, но мы не должны ее использовать using.
  2. ConfigureAwaitбыл введен в 4.5. Taskбыл введен в 4.0.
  3. Потоки .NET всегда использовались для передачи контекста (см. C # через книгу CLR), но в реализации по умолчанию Task.ContinueWithони не выполняются, поскольку выяснилось, что переключение контекста стоит дорого, и по умолчанию оно отключено.
  4. Проблема заключается в том, что разработчику библиотеки не нужно заботиться о том, нужен ли ее клиентам поток контекста или нет, следовательно, он не должен решать, следует ли передавать контекст контексту или нет.
  5. [Добавлено позже] Тот факт, что нет авторитетного ответа и правильной ссылки, и мы продолжаем бороться с этим, означает, что кто-то не выполнил свою работу правильно.

У меня есть несколько постов на эту тему, но в дополнение к хорошему ответу Тагберка, я предпочитаю, чтобы вы включили все API-интерфейсы асинхронно и в идеале передавали контекст. Поскольку вы выполняете асинхронное выполнение, вы можете просто использовать продолжения вместо ожидания, поэтому не будет возникать тупиковой ситуации, поскольку в библиотеке не выполняется ожидание, и вы продолжаете работу, поэтому контекст сохраняется (например, HttpContext).

Проблема в том, что библиотека предоставляет синхронный API, но использует другой асинхронный API - поэтому вам необходимо использовать Wait()/ Resultв своем коде.

Aliostad
источник
6
1) Вы можете позвонить, Task.Disposeесли хотите; Вам просто не нужно подавляющее большинство времени. 2) Taskбыла введена в .NET 4.0 как часть TPL, которая не нужна ConfigureAwait; когда они asyncбыли добавлены, они использовали существующий Taskтип вместо изобретения нового Future.
Стивен Клири
6
3) Вы путаете два разных типа «контекста». «Контекст», упомянутый в C # через CLR, всегда передается, даже в Tasks; «контекст», контролируемый ContinueWith- это SynchronizationContextили TaskScheduler. Эти различные контексты подробно объясняются в блоге Стивена Туба .
Стивен Клири
21
4) Автору библиотеки не нужно заботиться о том, нужен ли ее вызывающим элементам поток контекста, потому что каждый асинхронный метод возобновляется независимо. Таким образом, если вызывающие стороны нуждаются в потоке контекста, они могут передавать его независимо от того, передал ли автор библиотеки его или нет.
Стивен Клири
1
Сначала вы, кажется, жалуетесь, а не отвечаете на вопрос. И затем вы говорите о «контексте», за исключением того, что в .Net существует несколько видов контекста, и на самом деле неясно, о каком (или одном?) Вы говорите. И даже если вы сами не запутались (но я думаю, что это так, я думаю, что нет контекста, который имел обыкновение течь с Threads, но больше не с ним ContinueWith()), это затрудняет чтение вашего ответа.
svick
1
@StephenCleary да, lib dev не нужно знать, это зависит от клиента. Я думал, что ясно дал понять, но моя формулировка не была ясна.
Алиостад