Comments 27
Стратегия, когда библиотечный код полностью покрывается ConfigureAwait(false), изменяет поведение по умолчанию, и, что более важно, такой подход лишает разработчиков "клиентского" кода выбора.
А зачем этот самый выбор нужен? Варианта-то всего два:
Вариант 1. Библиотеке нужен контекст синхронизации. Тут и думать нечего — передача false будет ошибкой, допустимо только true.
Вариант 2. Библиотеке не нужен контекст синхронизации. Но в таком случае у ConfigureAwait(false)
нет ни одного недостатка кроме многословности! И раз уж вы решили "украсить" ваш код вызовами ConfigureAwait
— то нет смысла передавать туда что-то кроме false
.
Собственно, далее вы приводите примеры клиентских вызовов — и ни в одном из них не передаёте true для параметра continueOnCapturedContext
. Случайно ли это? Вы можете привести хоть один пример, когда в качестве этого параметра случает передавать true?
Выбор должен быть за клиентским кодом. Клиентский код не имеет доступа к исходникам библиотеки. Если клиентский код, в свою очередь, это тоже код библиотеки, то как ему указать что на всех уровнях надо (или же не надо) работать с контекстом?
async Task ClientFoo()
{
//На всех уровнях учитываем контекст синхронизации.
await FooAsync(
/*другие аргументы функции*/,
ancellationToken.None,
true);
}
Пример (7) показывает зачем нужен параметр, если бы разработчик метода FooAsync предусмотрел параметр, то клиент не встал бы на взаимоблокировке. А без параметра, можно лишь надеяться что внутри все верно, либо же писать дополнительный код, для обхода проблемы.
Библиотека, которой нужен этот самый контекст, будет использовать везде true (без всяких параметров), которой не нужен — false.
Учитывая, что асинхронное апи обычно связано с IO операциями, к UI они отношения не имеют и в 99% случаев передача false себя оправдывает.
Исключения с true обычно явные и заранее так и задуманные.
А зачем может понадобиться на всех уровнях использовать контекст синхронизации?
Если есть возможность встроить клиентский код в библиотеку (через события, колбеки, пайплайн, что угодно еще), то этот код может зависеть от контекста, и если перед ним вызвать .ConfigureAwait(false)
, он расстроится. Но! Библиотека все-таки должна знать, предоставляет она такую функциональность, или нет (и расставлять ConfigureAwait
в зависимости от этого).
Хороший (и грустный пример) — это логирование, которое берет данные для enrichment из Http-контекста, который, в свою очередь, недоступен без синхронизации. А библиотека об этом и не знает, она вообще не для веба писалась. Решение — брать enrichment из другого места, но об этом, блин, тоже подумать надо.
В таких случаях лучше не с ConfigureAwait баловаться, а явно или неявно (по аналогии с IProgress) передавать в библиотеку сам контекст синхронизации.
Что же до Http-контекста — в asp.net core он через статическое свойство больше не доступен.
Сам я везде в своих библиотеках всегда использую ConfigureAwait(false). Да, многословно местами, но это небольшая цена.
UPDATE: lair говорит выше именно об этом.
Эта проблема должна решаться через привязку делегата к контексту, а не через continueOnCapturedContext.
Хороший пример из стандартной библиотеки — System.Progress<>
.
Контекст синхронизации UI это просто способ запустить произвольный код в потоке UI. Вы передаёте в контекст функцию, он посылает сообщение в очередь сообщений UI потока, обработчик этого сообщения запускает вашу функцию. Можно придумать другие контексты, которые будут запускать ваши функции в пуле потоков или на удалённом сервере или ещё как нибудь.
Task может выполнять своё продолжение (код после await x.MethodAsync();) либо в пуле потоков, либо с помощью контекста (SynchronisationContext.Current). По умолчанию Task использует контекст. Также по умолчанию в UI приложениях контекст синхронизации выполняет код в UI потоке. В итоге если ничего не делать специально, то продолжение будет вызвано в UI потоке, а именно это обычно и нужно.
Загляните в официальную документацию, там все подробно описано.
В статье "Parallel Computing — It's All About the SynchronizationContext" дается информация о различных контекстах синхронизации.
Клиентскому коду абсолютно ни к чему управлять этим поведением. Если клиентскому коду нужен контекст — он может захватывать его на своем уровне. Библиотечный .ConfigureAwait(false) ему в этом никак не мешает.
Кажется, вы не до конца изучили вопрос. Либо же я его до конца не понимаю. Если вы сможете привести хоть один пример, когда клиентскому коду может потребоваться, чтобы библиотечный код захватывал контекст синхронизации внутри себя — я начну сомневаться в своих познаниях в этом вопросе=)
Я в своё время затупил и явно проверял (не нашел нигде в литературе пояснения) — а достаточно ли на уровне пользователя библиотеки сказать, что нужен контекст. И таки да, достаточно. Что бы там библиотека внутри не делала, если я сказал что после библиотечного вызова верни мне контекст, я хочу UI поменять — всё отлично работает.
Простой пример, когда библиотечный ConfigureAwait(false) все ломает: Если в приложении есть некоторые набор критических задач, который должны гарантированно работать, т.е. их приоритет высок, то таким задачам можно дать свой набор потоков, свою пул потоков. В этом случае контекст синхронизации важен, он не даст уйти из этого пула: все асинхронные вызовы будут возвращаться в этот "критический" пул, и продолжать работать на выделенных, для этого набора критических задач, потоках. Если же у вас библиотека внутри написана с ConfigureAwait(false), то вызовы ее методов, в конечном итоге, выходят из выделенного пула, на стандартный. Это значит, что если стандартный пул "просядет", то "просядут" и наши критические задача, а такого быть не должно.
Вырожденный случай — когда у нас есть одна критически важная операция, ей выделяют отдельный поток, который, условлено, никто не блокирует. Все действия этой операции должны выполняться в этом потоке, чтобы никакая нагрузка на стандартный пул эту задачу не задела.
В описанных ситуациях, четко видно, что разработчик клиентского кода, должен определять когда нужно использовать контекст, а когда не нужно. Клиентский код, а не код библиотеки.
Если задаче выделен высокоприоритетный поток — её надо исполнять полностью синхронно.
Потому что в противном случае избежать частичного попадания кода в пул потоков просто не получится, в стандартной библиотеке есть слишком много мест, которые от него зависят. Даже простейший вызов Task.Delay
и тот не способен работать без стандартного пула потоков.
Для того чтобы это понять, достаточно рассмотреть как работает контекст в WinForms, и добавить что очередь на обработку разбирается не одним потоком, а набором потоков из отдельного пула. И ConfigureAwait, в таком случае, сильно связан с вопросом, из какого пула будет взят поток для выполнения задачи.
Об этом, и не только, написано в статье "Parallel Computing — It's All About the SynchronizationContext". Достаточно просто внимательно ее прочесть.
Удалил комментарий, дублирует предыдущий.
В самых простых случаях вы верно описали ситуацию. Добавлю от себя о вопросе необходимости задания continueOnCapturedContext
. Реальность такова, что мы не используем библиотеки "атомарно". Как правило, выстроен целые стеки библиотек, эдакий слоеный пирог из функциональных слоев, где зачастую делегаты/события и прочие прелести передаются вверх и вниз по стеку вызовов. В таких ситуациях довольно сложно и накладно отслеживать, что кому нужно по контекстам синхронизации. Сейчас не нужно, в следующей версии библиотеки нужно. Невозможно, да и не стоит пытаться обременять вызывающие слои необходимостью задания continueOnCapturedContext
. Да, в некоторых редкий ситуациях, особенно с легаси-кодом, вариант с continueOnCapturedContext
может быть меньшим злом.
ConfigureAwait, кто виноват и что делать?