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

21

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

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

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

对于由阻塞代码调用的所有方法的传递闭包中的每个await,包括所有第三方和第二方代码,您都必须使用。

然而在文章后面,作者又写道:

预防死锁 有两种最佳实践方法(都包含在我的介绍帖子中),可以避免这种情况:
  1. 在您的“库”异步方法中,尽可能使用ConfigureAwait(false)
  2. 不要在Tasks上阻塞; 从上到下全部使用async
我看到了一些矛盾之处 - 在“不要这样做”部分中,他写道,必须在UI线程上阻塞时,需要使用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);
    }
}

@PeterDuniho 对于我的表述不够清晰,我深感抱歉。我已经更新了开头段落以更加精确地表达我的意思。我并不认为我误解了这篇文章——我只是在寻求澄清。 - Dai
5
如果我理解这篇文档正确的话,“如果某个 await 不在需要在 UI 线程上运行代码的方法中,我应该将 ConfigureAwait(false) 添加到每个 await ”是完全正确的。这样做并不是为了避免死锁,而是因为这是正确的做法,因为非 UI 代码等待 UI 线程可用是没有意义的。 - user743382
我可能会将您的VM中的调用更改为await Task.Run(() => this.client.PushDinglebopThroughGrumbo())。然后,您服务代码中的每个await都没有上下文可捕获,您就可以不再担心它了。如果该代码在库中,则应在所有地方放置ConfigureAwait(false) - Charles Mager
1个回答

8
如果我理解这份文档正确的话,我应该在每个不在需要运行在UI线程上的代码内部加入ConfigureAwait(false)来进行等待。是的,在UI应用程序中,默认的行为是使await之后的代码继续在UI线程上运行。当UI线程忙碌时,但你的代码不需要访问UI时,等待UI线程可用就没有意义了。(注:此处故意略去一些与本文不相关的细节。)但是如果我调用的任何第三方代码也在StreamReader上执行await操作呢?只要通过其他方法避免死锁,这只会影响性能而不是正确性。而潜在的性能不佳的第三方代码的问题并不是一个新问题。换句话说:要遵循最佳实践。
我也有点反感在每个地方都要放置ConfigureAwait(false),我认为await关键字的目的是消除显式的任务库调用,那么岂不应该有一个不同的关键字来进行无需继续上下文的等待吗?(例如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);
}

我在我的问题中添加了一个更新,提供了更好的“ConfigureAwait(false)-spam”问题示例 - 你能确认这是否是我应该这样做的吗? - Dai
@Dai 对于这个例子,如果我手动操作的话,我可能会使用 StringBuilder 来构建文件内容,然后发出一个单独的 WriteAsync。但是我可能会找到(或者甚至创建)一个序列化类,使用反射来遍历所有属性,查看属性以确定要写入的格式。这就是为 .NET 内置的将设置保存为 XML 的功能所做的事情,对于保存为文本也不应该有问题。 - user743382
不要使用await ExportSetting...,如果将每个ExportSetting调用添加到任务列表中[ myTaskList.Add(ExportSetting(wtr, setting.Key, setting.Value))],然后运行await Task.WhenAll(myTaskList).ConfigureAwait(false),您将获得更好的性能。 - Khelvaster
1
{btsdaf} - user743382
@hvd 真棒的观点。我错过了 ExportSetting 写入流的部分。 - Khelvaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接