Я прочитал эту статью 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);
}
}