我应该在每个等待的操作上调用ConfigureAwait(false)

Dai*_*Dai 16 c# async-await

我读了这篇文章https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html - 但是我看到了一个矛盾:

我知道UI线程死锁的问题,因为UI线程阻塞等待异步操作完成,但同样的异步操作被同步到UI线程上下文 - 因此异步操作无法进入UI线程,所以UI线程不会停止等待.

文章告诉我们,解决方法是不阻止UI线程,否则你需要在ConfigureAwait(false) 任何地方使用:

您必须在阻塞代码调用的所有方法的传递闭包中使用每个等待,包括所有第三方和第二方代码.

然而,作者后来在文章中写道:

防止死锁
有两个最佳实践(我的介绍帖中都有)避免了这种情况:

  1. 在"库"异步方法中,ConfigureAwait(false)尽可能使用.
  2. 不要阻止任务; 一直使用async.

我在这里看到一个矛盾 - 在"不要这样做"部分他写道,必须在ConfigureAwait(false)任何地方使用都是阻止UI线程的结果 - 但在他的"最佳实践"列表中,他告诉我们要做的只是那:" ConfigureAwait(false)尽可能使用." - 虽然我认为"只要有可能"会排除第三方代码,但是在没有第三方代码的情况下,如果我阻止UI线程,结果是相同的.

至于我的具体问题,这是我在WPF MVVM项目中的当前代码:

MainWindowViewModel.cs

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

    this.DisplayResponseInUI( response );
}
Run Code Online (Sandbox Code Playgroud)

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 );
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我正确地理解了文档,我应该添加ConfigureAwait(false)每个 await不在具有需要在UI线程上运行的代码的方法中 - 这是我PushDinglebopThroughGrumbo方法中的每个方法,而且还包括所有代码WebServiceResponse.FromResponse(调用await StreamReader.ReadLineAsync).但对于任何第三方的代码我呼吁还执行await的操作StreamReader?我将无法访问他们的源代码,因此这是不可能的.

不得不放在ConfigureAwait(false)任何地方,我也有点放松- 我认为await关键字的意思是消除显式的任务库调用 - 不应该有一个不同的关键字为resume-context-free等待吗?(例如awaitfree).

那么我的代码应该是这样的吗?

MainWindowViewModel.cs

(unmodified, same as above)
Run Code Online (Sandbox Code Playgroud)

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 );
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

...我原本以为ConfigureAwait(false)只有awaitPlumbusWebServiceClient方法内部的最顶层调用- 即调用时才需要GetAsync调用.

如果我确实需要在任何地方应用它,我可以将其简化为扩展方法吗?

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

using( HttpResponseMessage response = await _client.GetAsync( ... ).CF() )
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

......虽然这并没有减轻所有的繁琐.

更新:第二个例子

这里有一些我编写的异步代码将我的应用程序的设置导出到一个简单的文本文件 - 我不禁觉得它感觉不对,这真的是正确的方法吗?

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

小智 7

如果我正确理解了文档,我应该添加ConfigureAwait(false)到每个await不在具有需要在UI线程上运行的代码的方法中

是.UI应用程序中的默认行为是await继续在UI线程上执行的代码.当UI线程忙,但您的代码不需要访问UI时,没有必要等待UI线程可用.

(注意:这有意留下一些与此无关的细节.)

但对于任何第三方的代码我呼吁还执行await的操作StreamReader

只要您通过其他方式避免死锁,这只会影响性能,而不会影响正确性.并且可能性能不佳的第三方代码问题不是新问题.

换句话说:遵循两种最佳实践.

不得不放在ConfigureAwait(false)任何地方,我也有点放松- 我认为await关键字的意思是消除显式的任务库调用 - 不应该有一个不同的关键字为resume-context-free等待吗?(例如awaitfree).

ConfigureAwait 不是TPL方法.

await是一般化的,只要它们支持所需的方法,它就可以用于任意类型.对于随机示例,您可以为a添加扩展方法Task以返回允许后续代码在await新专用线程中继续的类型.这不需要带有new关键字的新版本编译器.

但是,这是一个很长的名字.

如果我确实需要在任何地方应用它,我可以将其简化为扩展方法吗?

是的,这完全没问题.


这里有一些我编写的异步代码将我的应用程序的设置导出到一个简单的文本文件 - 我不禁觉得它感觉不对,这真的是正确的方法吗?

正如我在评论中写的那样,我自己也不会使用这种方法......但是如果你真的想这样做,那么你可以在那里找到很多可以摆脱的代码重复.随着它的消失,它看起来不再那么糟糕了.

/* 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);
}
Run Code Online (Sandbox Code Playgroud)