Следует ли использовать ConfigureAwait (false) в библиотеках, которые вызывают асинхронные обратные вызовы?

Существует множество рекомендаций относительно того, когда использовать ConfigureAwait(false) при использовании await / async в C #.

Похоже, общая рекомендация - использовать ConfigureAwait(false) в коде библиотеки, поскольку это редко зависит от контекста синхронизации.

Однако предположим, что мы пишем очень общий служебный код, который принимает функцию в качестве входных данных. Простым примером могут быть следующие (неполные) функциональные комбинаторы, упрощающие выполнение простых операций, основанных на задачах:

Карта:

public static async Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> mapping)
{
    return mapping(await task);
}

FlatMap:

public static async Task<TResult> FlatMap<T, TResult>(this Task<T> task, Func<T, Task<TResult>> mapping)
{
    return await mapping(await task);
}

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

С одной стороны, если комбинаторы используются функционально, контекст синхронизации не требуется. С другой стороны, люди могут неправильно использовать API и делать контекстно-зависимые вещи в предоставленных функциях.

Один из вариантов - иметь отдельные методы для каждого сценария (Map и MapWithContextCapture или что-то в этом роде), но это выглядит некрасиво.

Другой вариант может заключаться в том, чтобы добавить параметр для отображения / плоской карты из и в ConfiguredTaskAwaitable<T>, но поскольку ожидаемым объектам не нужно реализовывать интерфейс, это приведет к большому количеству избыточного кода и, на мой взгляд, будет еще хуже.

Есть ли хороший способ передать ответственность вызывающей стороне, чтобы реализованной библиотеке не нужно было делать никаких предположений о том, нужен ли контекст в предоставленных функциях сопоставления?

Или это просто факт, что асинхронные методы не слишком хорошо сочетаются без различных предположений?

РЕДАКТИРОВАТЬ

Просто чтобы прояснить несколько вещей:

  1. Проблема действительно существует. Когда вы выполняете «обратный вызов» внутри служебной функции, добавление ConfigureAwait(false) приведет к нулевой синхронизации. контекст.
  2. Главный вопрос в том, как действовать в этой ситуации. Должны ли мы игнорировать тот факт, что кто-то может захотеть использовать синхронизацию. контекст, или есть хороший способ переложить ответственность на вызывающего, помимо добавления некоторой перегрузки, флага или чего-то подобного?

Как упоминается в нескольких ответах, можно было бы добавить к методу bool-flag, но, как я вижу, это тоже не слишком красиво, поскольку его придется распространять на всем протяжении API (поскольку есть больше "служебных" функций, в зависимости от показанных выше).


person nilu    schedule 04.05.2015    source источник
comment
Если вы хотите знать, какой текущий контекст находится в делегате при использовании ConfigureAwait(false), все, что вам нужно было сделать, это запустить код один раз с делегатом, который распечатывает текущий контекст, или даже просто код, который выйдет из строя, если исходный контекст не был захвачен. Это заняло бы у вас гораздо меньше времени, чем написание этого вопроса, учитывая, что у вас уже написан весь код.   -  person Servy    schedule 04.05.2015
comment
@Servy С этим не поспоришь. Тем не менее, понимание того, существует ли проблема или нет, не обязательно означает, что решение легко найти. И, надеюсь, обмен информацией не повредит.   -  person nilu    schedule 04.05.2015
comment
Вместо того, чтобы говорить, я не уверен, существует ли эта проблема, но если да, как мне ее решить? потратьте 30 секунд, чтобы выяснить, существует ли он, а затем спросите, как мне решить эту проблему, которую я обнаружил? (или не задавайте вопросов вообще, потому что проблемы не существует, в зависимости от того, что происходит).   -  person Servy    schedule 04.05.2015
comment
@Servy Я определенно провел тесты, чтобы убедиться, что возникла проблема. Вопрос в том, как с этим справиться, и нужно ли вообще с этим справляться. Если формулировка не говорит о том, что меня интересует более подробная информация, это ошибка с моей стороны.   -  person nilu    schedule 04.05.2015
comment
Вопрос, как я его читал, похоже, спрашивает, приведет ли добавление ConfigureAwait(false) к вашему методу к тому, что текущий контекст будет нулевым или ненулевым в обратном вызове. Если вы знаете, что оно не равно нулю, вы должны четко указать это в вопросе, который просто задаете, о том, как лучше всего справиться с этим фактом. В частности, ваш вопрос гласит: I am unsure how the context capture works wrt. closures. Это означает, что вы не знаете, что произойдет. Если вы знаете, но не знаете, как предоставить определенный набор функций при таком поведении, значит, именно здесь вам неясно.   -  person Servy    schedule 04.05.2015
comment
Связанный вопрос: stackoverflow.com/questions/13489065/   -  person jgauffin    schedule 12.08.2015


Ответы (3)


Когда вы говорите await task.ConfigureAwait(false), вы переходите к пулу потоков, в результате чего mapping запускается в нулевом контексте, а не в предыдущем контексте. Это может вызвать различное поведение. Итак, если звонивший написал:

await Map(0, i => { myTextBox.Text = i.ToString(); return 0; }); //contrived...

Тогда это выйдет из строя при следующей реализации Map:

var result = await task.ConfigureAwait(false);
return await mapper(result);

Но не здесь:

var result = await task/*.ConfigureAwait(false)*/;
...

Еще ужаснее:

var result = await task.ConfigureAwait(new Random().Next() % 2 == 0);
...

Подбросьте монетку о контексте синхронизации! Выглядит смешно, но не так абсурдно, как кажется. Более реалистичный пример:

var result =
  someConfigFlag ? await GetSomeValue<T>() :
  await task.ConfigureAwait(false);

Таким образом, в зависимости от некоторого внешнего состояния контекст синхронизации, в котором выполняется остальная часть метода, может измениться.

Это также может произойти с очень простым кодом, например:

await someTask.ConfigureAwait(false);

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

Этот недетерминизм - слабое место конструкции await. Это компромисс во имя производительности.

Самая досадная проблема здесь в том, что при вызове API непонятно, что происходит. Это сбивает с толку и вызывает ошибки.

Что делать?

Альтернатива 1: Вы можете возразить, что для обеспечения детерминированного поведения лучше всего всегда использовать task.ConfigureAwait(false).

Лямбда должна убедиться, что она работает в правильном контексте:

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext;
Map(..., async x => await Task.Factory.StartNew(
        () => { /*access UI*/ },
        CancellationToken.None, TaskCreationOptions.None, uiScheduler));

Вероятно, что-то из этого лучше скрыть в служебном методе.

Альтернатива 2: вы также можете утверждать, что функция Map не должна зависеть от контекста синхронизации. Следует просто оставить это в покое. Затем контекст перетекает в лямбду. Конечно, простое присутствие контекста синхронизации может изменить поведение Map (не в этом конкретном случае, а в целом). Так что Map должен быть разработан, чтобы справиться с этим.

Альтернатива 3: вы можете добавить логический параметр в Map, который указывает, передавать ли контекст или нет. Это сделало бы поведение явным. Это хороший дизайн API, но он загромождает API. Кажется неуместным рассматривать базовый API, такой как Map, с проблемами контекста синхронизации.

Какой маршрут выбрать? Я думаю, это зависит от конкретного случая. Например, если Map - вспомогательная функция пользовательского интерфейса, имеет смысл передать контекст. Если это библиотечная функция (например, помощник повтора), я не уверен. Я вижу, что все альтернативы имеют смысл. Обычно рекомендуется применять ConfigureAwait(false) во всем коде библиотеки. Следует ли делать исключение в тех случаях, когда мы вызываем обратные вызовы пользователей? Что делать, если мы уже вышли из правого контекста, например:

void LibraryFunctionAsync(Func<Task> callback)
{
    await SomethingAsync().ConfigureAwait(false); //Drops the context (non-deterministically)
    await callback(); //Cannot flow context.
}

Так что, к сожалению, нет простого ответа.

person usr    schedule 04.05.2015
comment
Usr, в свете этого связанного вопроса, считаете ли вы, что было бы неплохо вообще не использовать ConfigureAwait ( особенно, если код работает в ASP.NET) и предоставить клиенту (вызывающему верхнего уровня) возможность использовать Task.Run или что-то вроде WithNoContext для управления контекстом всей цепочки асинхронных вызовов? - person noseratio; 05.05.2015
comment
@Noseratio, что сделало бы Map более специализированной функцией, потому что теперь она имеет дело не только с бизнесом, но и с контекстом синхронизации. Если с архитектурной точки зрения все в порядке, это действительный маршрут. Хотя я не думаю, что это красиво. Я ненавижу скрытые зависимости от скрытого состояния. Наличие контекста синхронизации не прозрачно. Это существенно меняет семантику. - person usr; 05.05.2015
comment
Usr, я понимаю вашу точку зрения и склонен согласиться, хотя можно возразить, что библиотеки общего назначения обычно не зависят от контекста, то есть они не должны зависеть от текущего контекста или изменять его. - person noseratio; 05.05.2015
comment
@noseratio Мое мнение по этому поводу изменилось. Я склонен сейчас с тобой согласиться. Вы можете прочитать правку, если вам интересно. - person usr; 31.03.2019
comment
Хорошая поправка, @usr! Сам перестал использовать ConfigureAwait(false) в библиотеках. Затем, если я вызываю API и чувствую, что производительность зависит от контекста, я просто заключаю его в Task.Run. Кажется, однако, что в наши дни никого не волнует, как добиться дополнительной производительности с помощью таких настроек, как ConfigureAwait. Люди просто используют JavaScript как для внешнего, так и для внутреннего интерфейса, и, похоже, никого не волнует, как это работает "за кулисами" . По-видимому, намного проще / дешевле просто добавить в него больше виртуализированного оборудования, если это необходимо. - person noseratio; 31.03.2019

Вопрос в том, должны ли мы использовать ConfigureAwait (false) в этом случае?

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

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

Изменить:

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

Хорошая идея, предложенная @ i3arnon, - принять необязательный флаг bool, указывающий, нужен контекст или нет. Хотя это немного некрасиво, было бы неплохо обойтись.

person Yuval Itzchakov    schedule 04.05.2015
comment
Лямбда будет захватывать контекст в точке, где она вызывается. В этот момент контекст синхронизации может быть уже нулевым. - person usr; 04.05.2015
comment
Но пользователь предоставит ему этого делегата. Если ему нужен контекст, он должен его зафиксировать. - person Yuval Itzchakov; 04.05.2015
comment
Я, должно быть, неправильно тебя понял. - person usr; 04.05.2015
comment
@usr Если вы вызовете Map и предоставите ему делегат, возвращающий задачу, который учитывает контекст, он будет по-прежнему контекстно-зависимым, даже если я заключу его в await providedTask.ConfigureAwait(false) - person Yuval Itzchakov; 04.05.2015
comment
@YuvalItzchakov Но дело в том, что это заставляет делегата явно знать контекст, который он хочет использовать при создании делегата, а не полагаться на текущий контекст при вызове. Поскольку первое требует явной работы, а второе подразумевается в работе await, очень разумно изо всех сил поддерживать последнее. Просто потому, что вы можете заставить его работать, не означает, что вам нужно. Это лишает такой метод большей части полезности. - person Servy; 04.05.2015
comment
Оказывается, я даже неправильно понял вопрос. Я думал, что он хочет вызвать ConfigureAwait (false) для всего остального в своей служебной функции. Это приведет к переходу к пулу потоков (потенциально недетерминированно) и вызову делегата в разных контекстах. Это был бы хороший вопрос. Теперь вызов ConfigureAwait для результирующей задачи - гораздо более приземленный вопрос ... - person usr; 04.05.2015
comment
@usr Кажется, ты меня не неправильно понял. Я говорю о вызове ConfigureAwait (false) внутри самой служебной функции, а не в результате вызова служебной функции. - person nilu; 04.05.2015
comment
@nilu да. Когда вы говорите await task.CA(false), вы переходите к пулу потоков, в результате чего mapping запускается в нулевом контексте, а не в предыдущем контексте. Это вопрос? - person usr; 04.05.2015
comment
@usr Вы вызываете выполнение продолжения в потоке пула потоков. - person Yuval Itzchakov; 04.05.2015
comment
@usr Да, в каком-то смысле это как раз мой вопрос. Более интересный аспект, конечно, заключается в том, есть ли хороший способ каким-то необычным способом переложить ответственность на вызывающего абонента. - person nilu; 04.05.2015
comment
@nilu В моем ответе говорится о вызове служебной функции CA внутри tge, а не внутри делегата, предоставленного Task - person Yuval Itzchakov; 04.05.2015
comment
Нет, сам делегат работает на TP. Сделайте так, чтобы делегат async _ => { cw(threadid); } это увидел. - person usr; 04.05.2015
comment
Какой делегат? Тот, который заключен внутри задачи? - person Yuval Itzchakov; 04.05.2015
comment
@nilu Вы можете принять SynchronizationContext в методе и отправить в него, если он не равен нулю. Это может быть необязательный параметр со значением по умолчанию null. Или просто логическое значение, следует ли использовать ConfigureAwait. - person i3arnon; 04.05.2015

Я думаю, что настоящая проблема здесь связана с тем, что вы добавляете операции к Task, в то время как на самом деле вы работаете с их результатом.

Нет реальной причины дублировать эти операции для задачи в качестве контейнера вместо того, чтобы хранить их в результате задачи.

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

Если вместо этого Map реализуется следующим образом:

public static TResult Map<T, TResult>(this T value, Func<T, TResult> mapping)
{
    return mapping(value);
}

Вы можете легко использовать его с Task.ConfigureAwait или без него соответственно:

var result = await task.ConfigureAwait(false)
var mapped = result.Map(result => Foo(result));

Map это просто пример. Дело в том, чем вы здесь манипулируете. Если вы манипулируете задачей, вам не следует await передавать ее и передавать результат делегату-потребителю, вы можете просто добавить async логику, и вызывающий может выбрать, использовать Task.ConfigureAwait или нет. Если вы работаете на результат, вам не о чем беспокоиться.

Вы можете передать логическое значение каждому из этих методов, чтобы указать, хотите ли вы продолжить работу с захваченным контекстом или нет (или даже более надежно передать флаги параметров enum для поддержки других await конфигураций). Но это нарушает разделение ответственности, поскольку это не имеет ничего общего с Map (или его эквивалентом).

person i3arnon    schedule 04.05.2015
comment
Но в этом случае метод не имел бы никакого смысла - это был бы просто более подробный способ прямого вызова сопоставления. Хотя пример прост, я говорю о довольно фундаментальном способе абстрагироваться от задачи. - person nilu; 04.05.2015
comment
При всем уважении, этот метод Map бесполезен. Это могло быть просто Foo(await task). - person Sriram Sakthivel; 04.05.2015
comment
Это было бы полезно, когда метод имеет одну строку выполнения. Если ему нужно прооперировать возвращенный Task, он будет вынужден await - person Yuval Itzchakov; 04.05.2015
comment
@nilu есть разница, работаете ли вы над задачей или над результатом. Вы можете делать все, что хотите, с результатом, не беспокоясь (карта - это лишь пример). Когда вы работаете над задачей, вам, вероятно, не следует принимать делегатов-потребителей. - person i3arnon; 04.05.2015
comment
@SriramSakthivel Это бесполезно в самом примере. В этом-то и дело. Его не следует определять в Задаче. - person i3arnon; 04.05.2015
comment
@ i3arnon Я не спорю, полезен код или нет. Да, пример простой, но возможность функционально составлять асинхронные методы, безусловно, мне интересно. - person nilu; 04.05.2015
comment
@nilu Я не говорю о конкретном примере. Я говорю о разделении забот. Ваш служебный метод должен быть либо логическим методом для этого значения, либо async утилитой для Task. Проблемы возникают из-за того, что вы смешиваете их вместе. - person i3arnon; 04.05.2015
comment
@nilu вы можете передавать логическое значение каждый раз, чтобы указать, как должен вести себя await (или, что еще более надежно, перечисление параметров), но вам не нужно делать это для начала. - person i3arnon; 04.05.2015
comment
Я думаю, что этот ответ упускает суть, говоря, что Map определен неправильно. Это полностью надуманный пример. Конечно, вы не стали бы создавать такую ​​подпись, если бы единственная цель метода - воздействовать на результат. Более реалистичный пример передачи асинхронного делегата - это когда вам нужно отложенное / ленивое выполнение. то есть вызывающий определяет что, библиотечный метод определяет когда или даже если (и, возможно, что-то делает после). Это не может быть переделано на это. Я интерпретирую вопрос так: предполагая, что нам нужно дождаться задачи делегата в методе библиотеки, должны ли мы использовать ConfigureAwait(false)? - person Todd Menier; 25.09.2020
comment
@ToddMenier Использование ConfigureAwait в задаче делегата не влияет на код внутри делегата. Это влияет только на код библиотеки после этого ожидания. Проблема с рассматриваемым дизайном заключается в том, что он требует ожидания как расширенной задачи , так и асинхронного делегата, поскольку использование ConfigureAwait в задаче влияет на делегата. Это в основном делает этот метод похожим на ContinueWith, и поэтому для правильного выполнения требуется вся его выразительность. - person i3arnon; 26.09.2020
comment
@ i3arnon В этом есть смысл. После того, как я немного почесал голову (и перечитал вопрос), я явно неверно истолковал это. - person Todd Menier; 27.09.2020