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

时间:2016-08-23 16:05:44

标签: c# .net async-await synchronizationcontext

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

namespace MyLibrary1
{
    public class ClassFromMyLibrary1
    {
        public async Task<string> MethodFromMyLibrary1(string key, Func<string, Task<string>> 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<string> GetValueByKey(string key)
        {
            //simulate time-consuming operation
            await Task.Delay(500).ConfigureAwait(false);

            return string.Format("ValueFromRemoteLocationBy{0}", key);
        }
    }
}

我遵循了在我的图书馆中随处使用ConfigureAwait(false)(如this帖子中)的建议。然后我从我的测试应用程序中以同步方式使用它并获得失败:

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    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<string> 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()at   System.Windows.DependencyObject.SetValue(DependencyProperty dp,Object   System.Windows.Controls.ContentControl.set_Content(Object。)   值)在WpfApplication1.MainWindow.ActionToProcessNewValue(String   s)in   C:\ dev的\测试\ 4 \ WpfApplication1 \ WpfApplication1 \ MainWindow.xaml.cs:线   56点   MyLibrary1.ClassFromMyLibrary1.d__0.MoveNext()   在   C:\ dev的\测试\ 4 \ WpfApplication1 \ WpfApplication1 \ MainWindow.xaml.cs:线   77   ---从抛出异常的先前位置开始的堆栈跟踪结束--- at   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(任务   任务)   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务   任务)在System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()   在WpfApplication1.MainWindow.d__1.MoveNext()中   C:\ dev的\测试\ 4 \ WpfApplication1 \ WpfApplication1 \ MainWindow.xaml.cs:线   抛出异常:'System.InvalidOperationException'   WpfApplication1.exe

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

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

There is more detailed example of code解释了我必须处理的一些限制因素。

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

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

2 个答案:

答案 0 :(得分:3)

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

private async Task<string> 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<string> ActionToProcessNewValue(string s)
{
    Func<string> work = () => Label1.Content.ToString();
    if(Label1.Dispatcher.CheckAccess())
    {
        return Task.FromResult(work());
    }
    else
    {
        return Label1.Dispatcher.InvokeAsync(work, DispatcherPriority.Send).Task;
    }
}

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

答案 1 :(得分:1)

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

我将您的库API更改为:

namespace MyLibrary1
{
    public class ClassFromMyLibrary1
    {
        public async Task<string> 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<string> 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<string> ActionToProcessNewValue(string s)
    {
        Label1.Content = s;
        return Task.FromResult(string.Format("test2{0}", s));
    }