Подтвердить что ты не робот

Должны ли мы использовать ConfigureAwait (false) в библиотеках, вызывающих асинхронные обратные вызовы?

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

Похоже, что общая рекомендация заключается в использовании 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 или что-то еще), но он кажется уродливым.

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

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

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

ИЗМЕНИТЬ

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

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

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

4b9b3361

Ответ 1

Когда вы говорите 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.

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

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

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

Вероятно, лучше всего скрыть некоторые из них в утилите.

Это не изящное решение, но я думаю, что это наименьшее зло из возможных вариантов.


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

Ответ 2

Я думаю, что реальная проблема здесь возникает из-за того, что вы добавляете операции в 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 (или его эквивалентом).

Ответ 3

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

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

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

Edit:

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

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