Как стать автором
Обновить

Комментарии 27

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

А зачем этот самый выбор нужен? Варианта-то всего два:


Вариант 1. Библиотеке нужен контекст синхронизации. Тут и думать нечего — передача false будет ошибкой, допустимо только true.


Вариант 2. Библиотеке не нужен контекст синхронизации. Но в таком случае у ConfigureAwait(false) нет ни одного недостатка кроме многословности! И раз уж вы решили "украсить" ваш код вызовами ConfigureAwait — то нет смысла передавать туда что-то кроме false.


Собственно, далее вы приводите примеры клиентских вызовов — и ни в одном из них не передаёте true для параметра continueOnCapturedContext. Случайно ли это? Вы можете привести хоть один пример, когда в качестве этого параметра случает передавать true?

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


async Task ClientFoo()
{
    //На всех уровнях учитываем контекст синхронизации.
    await FooAsync(
        /*другие аргументы функции*/,
        ancellationToken.None,
        true);
}

Пример (7) показывает зачем нужен параметр, если бы разработчик метода FooAsync предусмотрел параметр, то клиент не встал бы на взаимоблокировке. А без параметра, можно лишь надеяться что внутри все верно, либо же писать дополнительный код, для обхода проблемы.

Главный вопрос — какой проблемы? Контекст синхронизации так навскидку обычно завязан на UI.
Библиотека, которой нужен этот самый контекст, будет использовать везде true (без всяких параметров), которой не нужен — false.
Учитывая, что асинхронное апи обычно связано с IO операциями, к UI они отношения не имеют и в 99% случаев передача false себя оправдывает.
Исключения с true обычно явные и заранее так и задуманные.

А зачем может понадобиться на всех уровнях использовать контекст синхронизации?

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


Хороший (и грустный пример) — это логирование, которое берет данные для enrichment из Http-контекста, который, в свою очередь, недоступен без синхронизации. А библиотека об этом и не знает, она вообще не для веба писалась. Решение — брать enrichment из другого места, но об этом, блин, тоже подумать надо.

В таких случаях лучше не с ConfigureAwait баловаться, а явно или неявно (по аналогии с IProgress) передавать в библиотеку сам контекст синхронизации.


Что же до Http-контекста — в asp.net core он через статическое свойство больше не доступен.

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

Таки AsyncLocal вроде поможет в такой ситуации? Оно правда тоже не дешево.

Это как раз означает, что кто-то должен подумать, и положить все, что нужно для логирования, в соответствующее хранилище.

Такие случаи действительно есть. Я периодически с ними сталкивался. Бывает, что библиотека получает какой-то делегат, например, это может быть метод получения значения для какого-нибудь класса кеша. Так вот когда библиотека будет запускать этот делегат «в свободном контексте» через ConfigureAwait(false), то клиентский код будет падать на таких конструкциях как HttpContext.Current. Они будут равны null. Нужно сказать, что это было довольно давно, еще до использования .NET Core. В нём в этом плане стало удобнее и безопаснее, код практически был переписан с оглядкой на набитые шишки. Кстати тогда я действительно рассматривал вариант добавить параметр continueOnCapturedContext в библиотечный метод и вроде бы даже так и сделал, уже не помню. Другим вариантом было сохранение значения из «проблемного» API заранее в локальную переменную и уже использование этой переменной через замыкание в делегате.

Сам я везде в своих библиотеках всегда использую ConfigureAwait(false). Да, многословно местами, но это небольшая цена.

UPDATE: lair говорит выше именно об этом.

Эта проблема должна решаться через привязку делегата к контексту, а не через continueOnCapturedContext.


Хороший пример из стандартной библиотеки — System.Progress<>.

Да, можно и так сказать, в этом суть. Ведь continueOnCapturedContext — эта та же самая привязка делегата к контексту, только окольным путем через вызов изнутри библиотеки. Ну и с возможными побочными эффектами для других методов, вызываемых внутри.

НЛО прилетело и опубликовало эту надпись здесь
Не совсем то, но есть CA2007: Do not directly await a Task — входит в пакет Microsoft.CodeAnalysis.FxCopAnalyzers.
Ещё бы что-то, где на пальцах про контекст синхронизации. Так чтобы было очень понятно. А то вроде понимаю, но есть ощущение, что не до конца.

Контекст синхронизации UI это просто способ запустить произвольный код в потоке UI. Вы передаёте в контекст функцию, он посылает сообщение в очередь сообщений UI потока, обработчик этого сообщения запускает вашу функцию. Можно придумать другие контексты, которые будут запускать ваши функции в пуле потоков или на удалённом сервере или ещё как нибудь.


Task может выполнять своё продолжение (код после await x.MethodAsync();) либо в пуле потоков, либо с помощью контекста (SynchronisationContext.Current). По умолчанию Task использует контекст. Также по умолчанию в UI приложениях контекст синхронизации выполняет код в UI потоке. В итоге если ничего не делать специально, то продолжение будет вызвано в UI потоке, а именно это обычно и нужно.


Загляните в официальную документацию, там все подробно описано.

Спасибо за пояснение. Уже более лучше.
А по поводу подробно — не значит понятно. Я потому и написал, что хотелось бы более популярного, на пальцах.
В корне не согласен с объявлением параметра continueOnCapturedContext в методах библиотечного кода. Это как раз библиотеке решать, нужен ей контекст или нет. Почти всегда — нет, поэтому и рекомендуется использовать .ConfigureAwait(false). Если все же нужен — используем .ConfigureAwait(true), либо вообще его не пишем.
Клиентскому коду абсолютно ни к чему управлять этим поведением. Если клиентскому коду нужен контекст — он может захватывать его на своем уровне. Библиотечный .ConfigureAwait(false) ему в этом никак не мешает.

Кажется, вы не до конца изучили вопрос. Либо же я его до конца не понимаю. Если вы сможете привести хоть один пример, когда клиентскому коду может потребоваться, чтобы библиотечный код захватывал контекст синхронизации внутри себя — я начну сомневаться в своих познаниях в этом вопросе=)
Всё правильно пишите.

Я в своё время затупил и явно проверял (не нашел нигде в литературе пояснения) — а достаточно ли на уровне пользователя библиотеки сказать, что нужен контекст. И таки да, достаточно. Что бы там библиотека внутри не делала, если я сказал что после библиотечного вызова верни мне контекст, я хочу UI поменять — всё отлично работает.

Это связано с тем, что за сохранение контекста отвечает не вызываемый код, а сам оператор await (точнее, скрытая за ним структура-awaiter).

Простой пример, когда библиотечный ConfigureAwait(false) все ломает: Если в приложении есть некоторые набор критических задач, который должны гарантированно работать, т.е. их приоритет высок, то таким задачам можно дать свой набор потоков, свою пул потоков. В этом случае контекст синхронизации важен, он не даст уйти из этого пула: все асинхронные вызовы будут возвращаться в этот "критический" пул, и продолжать работать на выделенных, для этого набора критических задач, потоках. Если же у вас библиотека внутри написана с ConfigureAwait(false), то вызовы ее методов, в конечном итоге, выходят из выделенного пула, на стандартный. Это значит, что если стандартный пул "просядет", то "просядут" и наши критические задача, а такого быть не должно.
Вырожденный случай — когда у нас есть одна критически важная операция, ей выделяют отдельный поток, который, условлено, никто не блокирует. Все действия этой операции должны выполняться в этом потоке, чтобы никакая нагрузка на стандартный пул эту задачу не задела.
В описанных ситуациях, четко видно, что разработчик клиентского кода, должен определять когда нужно использовать контекст, а когда не нужно. Клиентский код, а не код библиотеки.

Если задаче выделен высокоприоритетный поток — её надо исполнять полностью синхронно.


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

А я ничего не напутаю, если скажу, что контекст синхронизации и исполняющий поток — вещи из разных плоскостей? ConfigureAwait определяет необходимость захвата контекста синхронизации, а не использования того или иного потока. Т.е. использование continueOnCapturedContext для того, чтобы оставить код в потоке — концептуально неверное решение.

Для того чтобы это понять, достаточно рассмотреть как работает контекст в WinForms, и добавить что очередь на обработку разбирается не одним потоком, а набором потоков из отдельного пула. И ConfigureAwait, в таком случае, сильно связан с вопросом, из какого пула будет взят поток для выполнения задачи.
Об этом, и не только, написано в статье "Parallel Computing — It's All About the SynchronizationContext". Достаточно просто внимательно ее прочесть.

В самых простых случаях вы верно описали ситуацию. Добавлю от себя о вопросе необходимости задания continueOnCapturedContext. Реальность такова, что мы не используем библиотеки "атомарно". Как правило, выстроен целые стеки библиотек, эдакий слоеный пирог из функциональных слоев, где зачастую делегаты/события и прочие прелести передаются вверх и вниз по стеку вызовов. В таких ситуациях довольно сложно и накладно отслеживать, что кому нужно по контекстам синхронизации. Сейчас не нужно, в следующей версии библиотеки нужно. Невозможно, да и не стоит пытаться обременять вызывающие слои необходимостью задания continueOnCapturedContext. Да, в некоторых редкий ситуациях, особенно с легаси-кодом, вариант с continueOnCapturedContext может быть меньшим злом.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий