我读了这篇文章https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html,但是我看到了一个矛盾:
我知道在UI线程上阻塞等待异步操作完成会导致死锁问题,但是同样的异步操作又被同步到UI线程上下文中 - 因此异步操作无法进入UI线程,所以UI线程不会停止等待。
文章告诉我们解决方法是不要在UI线程上阻塞,否则你需要在每个地方使用ConfigureAwait(false)
:
对于由阻塞代码调用的所有方法的传递闭包中的每个await,包括所有第三方和第二方代码,您都必须使用。
然而在文章后面,作者又写道:
预防死锁 有两种最佳实践方法(都包含在我的介绍帖子中),可以避免这种情况:- 在您的“库”异步方法中,尽可能使用
ConfigureAwait(false)
。 - 不要在Tasks上阻塞; 从上到下全部使用
async
。
ConfigureAwait(false)
。但在他的“最佳实践”列表中,他告诉我们要做到这一点:“尽可能使用ConfigureAwait(false)
”。虽然我认为“尽可能使用”会排除第三方代码,但在没有第三方代码的情况下,如果我阻塞UI线程或不阻塞,结果是相同的。至于我的具体问题,在WPF MVVM项目中,这是我的当前代码:
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 );
}
}
}
如果我理解这个文件正确的话,我应该在每一个不在需要在UI线程上运行代码的方法中添加`ConfigureAwait(false)`到每一个`await`--也就是我`PushDinglebopThroughGrumbo`方法中的所有方法,还有`WebServiceResponse.FromResponse`(调用`await StreamReader.ReadLineAsync`)中的所有代码。但是对于任何第三方代码,在`StreamReader`上执行`await`操作怎么办?因为我无法访问他们的源代码,所以那是不可能的。
同时,我对必须到处放置`ConfigureAwait(false)`感到有点犹豫 - 我认为`await`关键字的目的是消除显式任务库调用--那么不应该有一个不同的关键字来进行恢复上下文自由等待吗?(例如:`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 );
}
}
}
我本以为只需要在
PlumbusWebServiceClient
方法中最顶层的await
调用 - 即GetAsync
调用 - 中使用ConfigureAwait(false)
。如果我确实需要在所有地方应用它,我能否将其简化为扩展方法?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);
}
}
await Task.Run(() => this.client.PushDinglebopThroughGrumbo())
。然后,您服务代码中的每个await
都没有上下文可捕获,您就可以不再担心它了。如果该代码在库中,则应在所有地方放置ConfigureAwait(false)
。 - Charles Mager