Должен ли я вызвать ConfigureAwait (false) для каждой ожидаемой операции

Я прочитал эту статью https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html - однако я вижу противоречие:

Я знаю проблему блокировки потока пользовательского интерфейса, потому что блоки потока пользовательского интерфейса ждут завершения операции async, но такая же операция async синхронизируется с контекстом потока пользовательского интерфейса - следовательно, операция async не может войти в поток пользовательского интерфейса, поэтому поток пользовательского интерфейса не остановится.

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

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

Однако позже в статье автор пишет:

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

  • В ваших асинхронных методах библиотеки используйте ConfigureAwait(false), где это возможно.
  • Не блокируйте Задачи; используйте async до конца.

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

Что касается моей конкретной проблемы, вот мой текущий код в проекте MVVM WPF:

MainWindowViewModel.cs

private async void ButtonClickEventHandler()
{
    WebServiceResponse response = await this.client.PushDinglebopThroughGrumbo();

    this.DisplayResponseInUI( response );
}

WebServiceClient.cs

public class PlumbusWebServiceClient {

    private static readonly HttpClient _client = new HttpClient();

    public async Task<WebServiceResponse> PushDinglebopThroughGrumbo()
    {
        try
        {
            using( HttpResponseMessage response = await _client.GetAsync( ... ) )
            {
                if( !response.IsSuccessStatusCode ) return WebServiceResponse.FromStatusCode( response.StatusCode );

                using( Stream versionsFileStream = await response.Content.ReadAsStreamAsync() )
                using( StreamReader rdr = new StreamReader( versionsFileStream ) )
                {
                    return await WebServiceResponse.FromResponse( rdr );
                }
            }
        }
        catch( HttpResponseException ex )
        {
            return WebServiceResponse.FromException( ex );
        }
    }
}

Если я правильно понимаю документ, я должен добавить ConfigureAwait(false) к каждому await, который не находится в методе, который должен иметь код, который должен запускаться в потоке пользовательского интерфейса - это каждый метод внутри моего метода PushDinglebopThroughGrumbo но и весь код в WebServiceResponse.FromResponse (который вызывает await StreamReader.ReadLineAsync). Но как насчет какого-либо третьего кода, который я вызываю, который также выполняет операции await на StreamReader? Я не буду иметь доступ к их исходному коду, чтобы это было невозможно.

Я также немного отключаюсь, когда нужно разместить ConfigureAwait(false) всюду - я думал, что точкой ключевого слова await было устранение явных вызовов библиотеки задач - не должно быть другого ключевого слова для resume- контекстно-свободный, ожидающий тогда? (например, awaitfree).

Так должен ли мой код выглядеть так?

MainWindowViewModel.cs

(unmodified, same as above)

WebServiceClient.cs

public class PlumbusWebServiceClient {

    private static readonly HttpClient _client = new HttpClient();

    public async Task<WebServiceResponse> PushDinglebopThroughGrumbo()
    {
        try
        {
            using( HttpResponseMessage response = await _client.GetAsync( ... ).ConfigureAwait(false) ) // <-- here
            {
                if( !response.IsSuccessStatusCode ) return WebServiceResponse.FromStatusCode( response.StatusCode );

                using( Stream versionsFileStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false) )  // <-- and here
                using( StreamReader rdr = new StreamReader( versionsFileStream ) )
                {
                    return await WebServiceResponse.FromResponse( rdr ).ConfigureAwait(false);  // <-- and here again, and inside `FromResponse` too
                }
            }
        }
        catch( HttpResponseException ex )
        {
            return WebServiceResponse.FromException( ex );
        }
    }
}

... Я бы подумал, что вызов ConfigureAwait(false) будет необходим только при самом верхнем вызове await внутри метода PlumbusWebServiceClient, то есть в вызове GetAsync.

Если мне нужно применить его повсюду, могу ли я упростить его до метода расширения?

public static ConfiguredTaskAwaitable<T> CF<T>(this Task<T> task) {
    return task.ConfigureAwait(false);
}

using( HttpResponseMessage response = await _client.GetAsync( ... ).CF() )
{
    ...
}

... хотя это не облегчает всю фидбъектность.

Обновление: второй пример

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

class Settings
{
    public async Task Export(String fileName)
    {
        using( StreamWriter wtr = new StreamWriter( fileName, append: false ) )
        {
            await ExportSetting( wtr, nameof(this.DefaultStatus     ), this.DefaultStatus                         ).ConfigureAwait(false);
            await ExportSetting( wtr, nameof(this.ConnectionString  ), this.ConnectionString                      ).ConfigureAwait(false);
            await ExportSetting( wtr, nameof(this.TargetSystem      ), this.TargetSystem.ToString("G")            ).ConfigureAwait(false);
            await ExportSetting( wtr, nameof(this.ThemeBase         ), this.ThemeBase                             ).ConfigureAwait(false);
            await ExportSetting( wtr, nameof(this.ThemeAccent       ), this.ThemeAccent                           ).ConfigureAwait(false);
            await ExportSetting( wtr, nameof(this.ShowSettingsButton), this.ShowSettingsButton ? "true" : "false" ).ConfigureAwait(false);
            await ExportSetting( wtr, nameof(this.ShowActionsColumn ), this.ShowActionsColumn  ? "true" : "false" ).ConfigureAwait(false);
            await ExportSetting( wtr, nameof(this.LastNameFirst     ), this.LastNameFirst      ? "true" : "false" ).ConfigureAwait(false);
            await ExportSetting( wtr, nameof(this.TitleCaseCustomers), this.TitleCaseCustomers ? "true" : "false" ).ConfigureAwait(false);
            await ExportSetting( wtr, nameof(this.TitleCaseVehicles ), this.TitleCaseVehicles  ? "true" : "false" ).ConfigureAwait(false);
            await ExportSetting( wtr, nameof(this.CheckForUpdates   ), this.CheckForUpdates    ? "true" : "false" ).ConfigureAwait(false);
        }
    }

    private static async Task ExportSetting(TextWriter wtr, String name, String value)
    {
        String valueEnc = Uri.EscapeDataString( value ); // to encode line-breaks, etc.

        await wtr.WriteAsync( name ).ConfigureAwait(false);
        await wtr.WriteAsync( '=' ).ConfigureAwait(false);
        await wtr.WriteLineAsync( valueEnc ).ConfigureAwait(false);
    }
}

Ответ 1

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

Да. Поведение по умолчанию в приложениях UI для кода после await для продолжения в потоке пользовательского интерфейса. Когда поток пользовательского интерфейса занят, но вашему коду не требуется доступ к пользовательскому интерфейсу, нет смысла ожидать, когда поток пользовательского интерфейса станет доступным.

(Примечание: это намеренно не учитывает некоторые детали, которые здесь не актуальны.)

Но как насчет какого-либо третьего кода, который я вызываю, который также выполняет операции await на StreamReader?

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

Другими словами: следуйте обеим рекомендациям.

Я также немного отключаюсь, когда мне приходится размещать ConfigureAwait(false) всюду - я думал, что точкой ключевого слова await было устранение явных вызовов библиотеки задач - не должно быть другого ключевого слова для resume- контекстно-свободный, ожидающий тогда? (например, awaitfree).

ConfigureAwait не является методом TPL.

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

Но да, это длинное имя.

Если мне нужно применить его повсюду, могу ли я упростить его до метода расширения?

Да, это прекрасно.


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

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

/* SettingsCollection omitted, but trivially implementable using
   Dictionary<string, string>, NameValueCollection,
   List<KeyValuePair<string, string>>, whatever. */

SettingsCollection GetAllSettings()
{
     return new SettingsCollection
     {
         { nameof(this.DefaultStatus     ), this.DefaultStatus                         },
         { nameof(this.ConnectionString  ), this.ConnectionString                      },
         { nameof(this.TargetSystem      ), this.TargetSystem.ToString("G")            },
         { nameof(this.ThemeBase         ), this.ThemeBase                             },
         { nameof(this.ThemeAccent       ), this.ThemeAccent                           },
         { nameof(this.ShowSettingsButton), this.ShowSettingsButton ? "true" : "false" },
         { nameof(this.ShowActionsColumn ), this.ShowActionsColumn  ? "true" : "false" },
         { nameof(this.LastNameFirst     ), this.LastNameFirst      ? "true" : "false" },
         { nameof(this.TitleCaseCustomers), this.TitleCaseCustomers ? "true" : "false" },
         { nameof(this.TitleCaseVehicles ), this.TitleCaseVehicles  ? "true" : "false" },
         { nameof(this.CheckForUpdates   ), this.CheckForUpdates    ? "true" : "false" }
     };
}

public async Task Export(String fileName)
{
    using( StreamWriter wtr = new StreamWriter( fileName, append: false ) )
        foreach (var setting in GetAllSettings())
            await ExportSetting( wtr, setting.Key, setting.Value ).ConfigureAwait(false);
}