ConfigureAwait(false)导致错误而不是死锁的情况

假设我编写了一个依赖于async方法的库:

 namespace MyLibrary1 { public class ClassFromMyLibrary1 { public async Task MethodFromMyLibrary1(string key, Func<string, Task> actionToProcessNewValue) { var remoteValue = await GetValueByKey(key).ConfigureAwait(false); //do some transformations of the value var newValue = string.Format("Remote-{0}", remoteValue); var processedValue = await actionToProcessNewValue(newValue).ConfigureAwait(false); return string.Format("Processed-{0}", processedValue); } private async Task GetValueByKey(string key) { //simulate time-consuming operation await Task.Delay(500).ConfigureAwait(false); return string.Format("ValueFromRemoteLocationBy{0}", key); } } } 

我遵循了在我的库中使用ConfigureAwait(false)的建议(就像在这篇文章中一样)。 然后我从我的测试应用程序以同步方式使用它并获得失败:

 namespace WpfApplication1 { ///  /// Interaction logic for MainWindow.xaml ///  public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Button1_OnClick(object sender, RoutedEventArgs e) { try { var c = new ClassFromMyLibrary1(); var v1 = c.MethodFromMyLibrary1("test1", ActionToProcessNewValue).Result; Label2.Content = v1; } catch (Exception ex) { System.Diagnostics.Trace.TraceError("{0}", ex); throw; } } private Task ActionToProcessNewValue(string s) { Label1.Content = s; return Task.FromResult(string.Format("test2{0}", s)); } } } 

失败的是:

WpfApplication1.vshost.exe错误:0:System.InvalidOperationException:调用线程无法访问此对象,因为另一个线程拥有它。 在System.Windows.Threading.Dispatcher.VerifyAccess()处于System.Windows.DependencyObject.SetValue(DependencyProperty dp,Object value)的System.Windows.Controls.ContentControl.set_Content(Object value)at WpfApplication1.MainWindow.ActionToProcessNewValue(String s) )在C:\ dev \ tests \ 4 \ WpfApplication1 \ WpfApplication1 \ MainWindow.xaml.cs:第56行,位于C:\ dev \ tests \ 4 \ WpfApplication1 \ WpfApplication1 \ MainWindow.xaml中的MyLibrary1.ClassFromMyLibrary1.d__0.MoveNext() .cs:第77行—从抛出exception的上一个位置开始的堆栈跟踪结束—在System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务任务)的System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(任务任务)中at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()at WpfApplication1.MainWindow.d__1.MoveNext()in C:\ dev \ tests \ 4 \ WpfApplication1 \ WpfApplication1 \ MainWindow.xaml.cs:line 39抛出exception:’ WpfApplication1.exe中的System.InvalidOperationException’

显然错误发生是因为我的库中的等待者丢弃了当前的WPF上下文。

另一方面,在删除库中的ConfigureAwait(false)后,我显然会遇到死锁。

有更详细的代码示例解释了我必须处理的一些约束。

那么我该如何解决这个问题呢? 这里最好的方法是什么? 我还需要遵循有关ConfigureAwait的最佳做法吗?

PS,在实际场景中,我有很多类和方法,因此在我的库中有很多这样的异步调用。 几乎不可能找出某个特定的异步调用是否需要上下文(请参阅@Alisson响应的注释)来修复它。 我不关心性能,至少在这一点上。 我正在寻找一些解决这个问题的一般方法。

通常情况下,库会记录回调是否会保证在调用它的同一个线程上,如果没有记录,最安全的选项是假设它没有。 您的代码示例(以及您从我的评论中可以看出的第三方)属于“不保证”类别。 在这种情况下,您只需要检查是否需要在回调方法中执行Invoke并执行它,您可以调用Dispatcher.CheckAccess() ,如果您需要在使用控件之前调用它将返回false

 private async Task ActionToProcessNewValue(string s) { //I like to put the work in a delegate so you don't need to type // the same code for both if checks Action work = () => Label1.Content = s; if(Label1.Dispatcher.CheckAccess()) { work(); } else { var operation = Label1.Dispatcher.InvokeAsync(work, DispatcherPriority.Send); //We likely don't need .ConfigureAwait(false) because we just proved // we are not on the UI thread in the if check. await operation.Task.ConfigureAwait(false); } return string.Format("test2{0}", s); } 

这是一个备用版本,具有同步回调而不是异步回调。

 private string ActionToProcessNewValue(string s) { Action work = () => Label1.Content = s; if(Label1.Dispatcher.CheckAccess()) { work(); } else { Label1.Dispatcher.Invoke(work, DispatcherPriority.Send); } return string.Format("test2{0}", s); } 

如果你想从Label1.Content获取值而不是分配它,这是另一个版本,这也不需要在回调中使用async / await。

 private Task ActionToProcessNewValue(string s) { Func work = () => Label1.Content.ToString(); if(Label1.Dispatcher.CheckAccess()) { return Task.FromResult(work()); } else { return Label1.Dispatcher.InvokeAsync(work, DispatcherPriority.Send).Task; } } 

重要说明:如果您没有删除按钮单击处理程序中的.Result ,则所有这些方法都会导致程序死锁,回调中的Dispatcher.InvokeDispatcher.InvokeAsync在等待时永远不会启动.Result返回和.Result在等待回调返回时永远不会返回。 您必须将单击处理程序更改为async void并执行await而不是.Result

实际上,你在ClassFromMyLibrary1收到一个回调,你不能假设它会做什么(比如更新一个Label)。 您的类库中不需要ConfigureAwait(false) ,因为您提供的链接给我们提供了如下解释:

随着异步GUI应用程序变得越来越大,您可能会发现许多使用GUI线程作为其上下文的异步方法的一小部分。 这可能导致迟缓,因为响应性受到“数千次剪纸”的影响。

要缓解这种情况,请尽可能等待ConfigureAwait的结果。

通过使用ConfigureAwait,您可以启用少量并行:一些异步代码可以与GUI线程并行运行,而不是通过一些工作来不断地对其进行篡改。

现在请阅读:

在需要上下文的方法中的await之后有代码时,不应使用ConfigureAwait。 对于GUI应用程序,这包括操作GUI元素,写入数据绑定属性或依赖于特定于GUI的类型(如Dispatcher / CoreDispatcher)的任何代码。

你做的恰恰相反。 您尝试在两个点上更新GUI,一个在回调方法中,另一个在此处:

 var c = new ClassFromMyLibrary1(); var v1 = c.MethodFromMyLibrary1("test1", ActionToProcessNewValue).Result; Label2.Content = v1; // updating GUI... 

这就是删除ConfigureAwait(false)解决问题的原因。 此外,您可以使按钮单击处理程序异步并等待ClassFromMyLibrary1方法调用。

在我看来,您应该重新设计库API,以便不将基于回调的API与基于任务的API混合。 至少在你的示例代码中,没有令人信服的案例可以做到这一点而且你已经找到了一个不这样做的理由 – 很难控制你的回调运行的上下文。

我将您的库API更改为:

 namespace MyLibrary1 { public class ClassFromMyLibrary1 { public async Task MethodFromMyLibrary1(string key) { var remoteValue = await GetValueByKey(key).ConfigureAwait(false); return remoteValue; } public string TransformProcessedValue(string processedValue) { return string.Format("Processed-{0}", processedValue); } private async Task GetValueByKey(string key) { //simulate time-consuming operation await Task.Delay(500).ConfigureAwait(false); return string.Format("ValueFromRemoteLocationBy{0}", key); } } } 

并称之为:

  private async void Button1_OnClick(object sender, RoutedEventArgs e) { try { var c = new ClassFromMyLibrary1(); var v1 = await c.MethodFromMyLibrary1("test1"); var v2 = await ActionToProcessNewValue(v1); var v3 = c.TransformProcessedValue(v2); Label2.Content = v3; } catch (Exception ex) { System.Diagnostics.Trace.TraceError("{0}", ex); throw; } } private Task ActionToProcessNewValue(string s) { Label1.Content = s; return Task.FromResult(string.Format("test2{0}", s)); }