我是否应该在每个等待的操作

时间:2017-04-15 08:02:11

标签: 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.   
  3. 不要阻止任务;一直使用async
  4.   

我在这里看到了一个矛盾 - 在"不要这样做"他写道,必须在任何地方使用ConfigureAwait(false)都是阻止UI线程的结果 - 但在他的“最佳实践”中#34;然后,他告诉我们这样做:"尽可能使用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 );
        }
    }
}

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

我不得不将await放在任何地方 - 我认为awaitfree关键字的意思是消除明确的任务库调用 - 我不应该有点放弃 - 不应该这样做还有一个不同的关键字,等待恢复 - 上下文免费? (例如(unmodified, same as above) )。

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

MainWindowViewModel.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 );
        }
    }
}

WebServiceClient.cs

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);
    }
}

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

更新:第二个例子

以下是我编写的一些异步代码,它将我的应用程序的设置导出到一个简单的文本文件中 - 我无法帮助,但认为它感觉不对,这是否真的是正确的方法这样做?

 POST http://127.0.0.1:8000/comments 500 (Internal Server Error)

1 个答案:

答案 0 :(得分:7)

  

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

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

(注意:这有意遗漏了一些与此无关的细节。)

  

但是我调用的任何第三方代码如何对await执行StreamReader操作呢?

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

换句话说:遵循两个最佳做法。

  

我不得不将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);
}