Использование ConfigureAwait в .NET

Я читал о ConfigureAwait в разных местах (включая вопросы SO), и вот мои выводы:

  • ConfigureAwait (true): выполняет остальную часть кода в том же потоке, что и код до запуска await.
  • ConfigureAwait (false): выполняет остальную часть кода в том же потоке, в котором был запущен ожидаемый код.
  • Если за ожиданием следует код, обращающийся к пользовательскому интерфейсу, к задаче следует добавить .ConfigureAwait(true). В противном случае возникнет исключение InvalidOperationException из-за доступа другого потока к элементам пользовательского интерфейса.

Мои вопросы:

  1. Мои выводы верны?
  2. Когда ConfigureAwait (false) улучшает производительность, а когда нет?
  3. Если вы пишете для приложения с графическим интерфейсом, но следующие строки не имеют доступа к элементам пользовательского интерфейса. Что следует использовать: ConfigureAwait (false) или ConfigureAwait (true)?

person Youssef13    schedule 01.07.2020    source источник
comment
Я не рекомендую использовать SO в качестве основного источника информации об async / await. здесь много дезинформации от людей, которые думают, что знают, как это работает. Используйте документацию и сочинения Стивена Туба, Стивена Клири, Джона Скита. Но, вопреки моему совету, вы можете найти одно из моих сообщений в блоге было полезным.   -  person Crowcoder    schedule 01.07.2020
comment
Отвечает ли это на ваш вопрос? Как и когда использовать async и await   -  person Michael Searson    schedule 01.07.2020
comment
@MichaelSearson Это не объясняет производительность.   -  person Youssef13    schedule 01.07.2020
comment
Are my conclusions correct - нет. Async - это не потоки. Не существует прямого соответствия между ConfigureAwait и использованием определенного потока. См., Например, stackoverflow.com/q/46094134/11683.   -  person GSerg    schedule 01.07.2020
comment
@GSerg Насколько я понимаю, смысл ConfigureAwait заключается в том, выполняется ли следующий код в методе в основном потоке или нет. В случаях, когда доступ к элементам пользовательского интерфейса осуществляется после ожидания задачи, он может вызвать исключение InvalidOperationException, если задача вызывается с ConfigureAwait (false). Насколько я понимаю, причина в том, что обновления пользовательского интерфейса запускались из другого потока. Если выводы неверны, просьба сообщить подробности.   -  person Youssef13    schedule 01.07.2020
comment
Вы должны определить, что вы имеете в виду под производительностью. Асинхронный код на самом деле немного менее производительный, чем тот же неасинхронный код. Основные преимущества заключаются в масштабируемости и параллелизме, особенно с кодом, привязанным к вводу-выводу.   -  person Crowcoder    schedule 01.07.2020
comment
@ Youssef13 Async занимается контекстами синхронизации, а не потоками. Использование или неиспользование отдельного потока является деталью реализации, если явно не задокументировано иное (например, с Task.Run()). ConfigureAwait(false) предполагает возобновление без захвата контекста синхронизации, а не требует этого. Для некоторых контекстов синхронизации (например, того, который используется в приложении Winforms), возобновление в том же контексте означает выполнение в потоке пользовательского интерфейса. Для других контекстов это может не означать этого или вообще не иметь наблюдаемого эффекта.   -  person GSerg    schedule 01.07.2020
comment
@Crowcoder Я не сравниваю асинхронный код с синхронным. Я сравниваю два асинхронных кода. Один с ConfigureAwait (true), а другой с ConfigureAwait (false). В этой статье говорится, что ConfigureAwait (false) повышает производительность. Мой вопрос: когда это на самом деле улучшает, а когда нет?   -  person Youssef13    schedule 01.07.2020
comment
@ Youssef13 да, хорошо, что ты нашел Стивена Туба. Он говорит о горячем пути, который, вероятно, к вам не относится, что прирост производительности, вероятно, будет слишком мал для измерения (просто предположение, не видя вашего кода). Не используйте ConfigureAwait(false) для контекстно-зависимого кода, например того, что должно выполняться в потоке пользовательского интерфейса после завершения. Другой пример - доступ к HttpContext в приложении ASP.NET WebForms. Ссылка, которую я опубликовал, указывает на приложение WPF, которое исследует эти вещи, в том числе то, как вы все еще можете заблокироваться при использовании ConfigureAwait (false) с небрежным кодом библиотеки.   -  person Crowcoder    schedule 01.07.2020
comment
Для получения дополнительной информации о ConfigureAwait и параллелизме я рекомендую книгу Стивена Клири «Параллелизм в C # Cookbook».   -  person Vladislav    schedule 01.07.2020


Ответы (3)


Чтобы ответить на ваши вопросы более прямо:

ConfigureAwait (true): выполняет остальную часть кода в том же потоке, что и код до запуска await.

Не обязательно один и тот же поток, но тот же контекст синхронизации. Контекст синхронизации может решить, как запускать код. В приложении UI это будет тот же поток. В ASP.NET это может быть не тот же поток, но у вас будет доступный HttpContext, как и раньше.

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

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

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

Неправильно добавлять к нему .ConfigureAwait(true). ConfigureAwait(true) - значение по умолчанию. Так что, если это то, что вы хотите, вам не нужно это указывать.

  1. Когда ConfigureAwait (false) улучшает производительность, а когда нет?

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

  1. Если вы пишете для приложения с графическим интерфейсом, но следующие строки не имеют доступа к элементам пользовательского интерфейса. Что следует использовать: ConfigureAwait (false) или ConfigureAwait (true)?

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

  1. Я сомневаюсь, что вы заметите какое-либо улучшение производительности.
  2. Это может привести к параллелизму, которого вы не ожидаете. Если вы используете ConfigureAwait(false), продолжение может выполняться в любом потоке, поэтому у вас могут возникнуть проблемы при доступе к небезопасным для потоков объектам. Такие проблемы возникают нечасто, но могут случиться.
  3. Вы (или кто-то другой, поддерживающий этот код) можете добавить код, который будет взаимодействовать с пользовательским интерфейсом позже, и возникнут исключения. Надеюсь, ConfigureAwait(false) легко обнаружить (это может быть другой метод, отличный от того, где выбрасывается исключение), и вы / они знаете, что он делает.

Я считаю, что ConfigureAwait(false) проще вообще не использовать (кроме библиотек). По словам Стивена Туба (сотрудника Microsoft) в ConfigureAwait FAQ:

При написании приложений обычно требуется поведение по умолчанию (вот почему это поведение по умолчанию).

person Gabriel Luci    schedule 02.07.2020
comment
Привет, Габриэль, я рад, что ты жив! Вы исчезли из SO на год или около того. Мне не хватало изображения мирной коровы на пастбищах. ???? - person Theodor Zoulias; 10.06.2021
comment
@TheodorZoulias Привет! Я удивлен, что ты заметил. Я скрывался, но прошедший год был беспокойным и напряженным, поэтому у меня не было времени отвечать на какие-либо вопросы. - person Gabriel Luci; 11.06.2021

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

ConfigureAwait(false) рекомендуется везде, где возврат к тому же контексту синхронизации (который обычно связан с потоком) не требуется, особенно в библиотеках, которые ожидают чего-то внутри: https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f.

ConfigureAwait(true) (по умолчанию) необходим, когда вам нужен тот же контекст, но в определенных ситуациях может также привести к мертвой блокировке.

Рассмотрим этот код:

void Main()
{
    // creating a windows form attaches a synchronization context to the current thread
    new System.Windows.Forms.Form();
    var task = DoSth();
    Console.WriteLine(task.Result);
}

async Task<int> DoSth()
{
    await Task.Delay(1000);
    return 1;
}

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

person Adassko    schedule 01.07.2020
comment
Сказать, что ConfigureAwait (true) рекомендуется в библиотеках, меня больше смущает. - person Youssef13; 01.07.2020
comment
@ Youssef13: это потому, что вы не можете контролировать, будет ли разработчик использовать вашу библиотеку должным образом. Добавляя .ConfigureAwait(false) во внутренние вызовы библиотеки, вы предотвращаете потенциальные взаимоблокировки, когда разработчик хочет использовать вашу библиотеку синхронно (используя .Result). Конечно, нельзя использовать задачи таким образом, но проблема в глазах пользователя будет создаваться вашей библиотекой. - person Adassko; 01.07.2020
comment
@Adassko, в своем ответе вы указали ConfigureAwait (true). Может опечатка? - person Youssef13; 01.07.2020
comment
@ Youssef13 да, я хотел написать про (true), но удалил его и начал печатать про (false). Виноват - person Adassko; 01.07.2020
comment
@Adassko, Итак, должен ли я всегда использовать ConfigureAwait (false), если возвращение к тому же SynchronizationContext не требуется? Это всегда улучшает производительность? Если нет, то когда это не улучшает производительность? (Я не получил первую строчку ответа) - person Youssef13; 01.07.2020
comment
Рекомендовать .ConfigureAwait(false) как средство от взаимоблокировок - это плохо. - person GSerg; 01.07.2020
comment
Я все еще в замешательстве и не могу понять, когда использовать false, а когда использовать true. Я в настоящее время думаю, что всегда используйте false, когда это возможно (это возможно для библиотечного кода). И используйте true, если вам нужно вернуться к тому же SynchronizationContext. Но не уверен в том случае, когда я нахожусь в приложении с графическим интерфейсом, но следующие строки не имеют доступа к элементам пользовательского интерфейса. - person Youssef13; 01.07.2020
comment
@ Youssef13, ты не должен слишком об этом заботиться. Приятно добавить его в библиотеки, чтобы избежать взаимоблокировок. ConfigureAwait (false) никогда не работает медленнее и может использоваться всегда, когда контекст не нужен - есть даже решения, чтобы всегда добавлять его ко всем вызовам, например, этот плагин fody: github.com/Fody/ConfigureAwait. Но меня вообще не волнует производительность, если это не проблема для вас. false может значительно улучшить производительность только тогда, когда поток контекста синхронизации действительно занят и требуется время, чтобы его дождаться, что почти никогда не бывает - person Adassko; 01.07.2020
comment
@ Youssef13 Если вы не знаете, как его использовать, просто не обращайте внимания на то, что ConfigureAwait существует (кроме библиотек). В обычных приложениях он вам редко когда-либо понадобится. Я никогда этим не пользуюсь. Никакого улучшения производительности не будет заметно. Недавно я написал подробный ответ по этому поводу. - person Gabriel Luci; 02.07.2020

Использование ConfigureAwait(false) в коде приложения обычно не способствует значительному повышению производительности вашего приложения, потому что обычно вы не await внутри циклов в коде приложения. Например, давайте рассмотрим случай, когда ваше приложение имеет кнопку и асинхронная операция запускается каждый раз, когда пользователь нажимает кнопку, а асинхронная операция включает в себя один await. Вводя 22 символа .ConfigureAwait(false) после этого await, вы уже потеряли сопоставимое время своей жизни, причем время, которое вы можете надеяться сэкономить, от 10 пользователей, которые нажимают эту кнопку каждую минуту, 8 часов в день, в течение 20 лет каждый (~ 35000000 переключений контекста всего = несколько секунд процессорного времени).

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

С другой стороны, если ваш Button_Click обработчик содержит такой код:

private async void Button_Click(object sender, EventArgs e)
{
    var client = new WebClient();
    using var stream = await client.OpenReadTaskAsync("someUrl");
    var buffer = new byte[1024];
    while ((await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
    {
        //...
    }
}

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

person Theodor Zoulias    schedule 01.07.2020