ConfigureAwait(false)导致错误而不是死锁的情况 [英] A case when ConfigureAwait(false) causes an error instead of deadlock

查看:104
本文介绍了ConfigureAwait(false)导致错误而不是死锁的情况的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Suppose I have written a library which relies on async methods:

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)的建议(例如发布)在我图书馆的任何地方.然后我从测试应用程序以同步的方式使用它,但失败了:

I followed the recommendations of using ConfigureAwait(false) (like in this post) everywhere in my library. Then I use it in synchronous way from my test app and get a failure:

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()位于 System.Windows.DependencyObject.SetValue(DependencyProperty dp,Object 值)在System.Windows.Controls.ContentControl.set_Content(Object 值)在WpfApplication1.MainWindow.ActionToProcessNewValue(String s)在 C:\ dev \ tests \ 4 \ WpfApplication1 \ WpfApplication1 \ MainWindow.xaml.cs:line 56岁 MyLibrary1.ClassFromMyLibrary1.d__0.MoveNext() 在 C:\ dev \ tests \ 4 \ WpfApplication1 \ WpfApplication1 \ MainWindow.xaml.cs:line 77 ---从之前引发异常的位置开始的堆栈跟踪- System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(任务 任务) System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务 任务),位于System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() 在WpfApplication1.MainWindow.d__1.MoveNext()中 C:\ dev \ tests \ 4 \ WpfApplication1 \ WpfApplication1 \ MainWindow.xaml.cs:line 39抛出异常:"System.InvalidOperationException" WpfApplication1.exe

WpfApplication1.vshost.exe Error: 0 : System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it. at System.Windows.Threading.Dispatcher.VerifyAccess() at System.Windows.DependencyObject.SetValue(DependencyProperty dp, Object value) at System.Windows.Controls.ContentControl.set_Content(Object value) at WpfApplication1.MainWindow.ActionToProcessNewValue(String s) in C:\dev\tests\4\WpfApplication1\WpfApplication1\MainWindow.xaml.cs:line 56 at MyLibrary1.ClassFromMyLibrary1.d__0.MoveNext() in C:\dev\tests\4\WpfApplication1\WpfApplication1\MainWindow.xaml.cs:line 77 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) 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 thrown: 'System.InvalidOperationException' in WpfApplication1.exe

很明显,由于我的库中的等待者丢弃了当前的WPF上下文,所以发生了错误.

Obviously the error happens because the awaiters in my library discard current WPF context.

另一方面,在库中的所有地方删除ConfigureAwait(false)之后,我显然会出现死锁.

From the other hand, after removing the ConfigureAwait(false) everywhere in the library I obviously get a deadlock instead.

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

There is more detailed example of code which explains some constraints that I have to deal with.

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

So how can I address this issue? What is the best approach here? Do I still need to follow the best practice regarding ConfigureAwait?

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

PS, In real scenario I have many classes and methods therefore tons of such async calls in my library. It's nearly impossible to find out if some particular async call requires context or not (see comments to @Alisson response) to fix it. I don't care about performance though, at least at this point. I'm looking for some general approach to address this issue.

推荐答案

通常情况下,如果可以保证回调位于与调用它相同的线程上,则库将记录该文件,如果未记录该回调,则最安全的选择是假定它不是.您的代码示例(根据您的评论,我可以确定您所合作的第三方)属于不保证"类别.在这种情况下,您只需要检查是否需要在回调方法中执行Invoke并执行它,就可以调用Dispatcher.CheckAccess(),如果需要在使用控件之前调用它,它将返回false./p>

Normally a library will document if a callback will be guaranteed to be on the same thread that called it, if it is not documented the safest option will be to assume it does not. Your code example (and the 3rd party you are working with from what I can tell from your comments) fall under the category of "Not guaranteed". In that situation you just need to check if you need to do a Invoke from inside the callback method and do it, you can call Dispatcher.CheckAccess() and it will return false if you need to invoke before using the control.

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

这是具有同步回调而不是异步回调的备用版本.

Here is a alternate version with a syncronous callback instead of a async one.

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.

Here is another version if you wanted to get the value from Label1.Content instead of assigning it, this also does not need to use async/await inside the callback.

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

重要说明:如果您没有摆脱按钮点击处理程序中的.ResultDispatcher.InvokeDispatcher.InvokeAsync,所有这些方法将导致程序死锁回调中的.Result永远不会开始,而.Result则是等待回调返回时永远不会返回.您必须将点击处理程序更改为async void并执行await而不是.Result.

IMPORTANT NOTE: all of these methods will cause your program to deadlock if you don't get rid of the .Result in the button click handler, the Dispatcher.Invoke or the Dispatcher.InvokeAsync in the callback will never start while it is waiting for .Result to return and .Result will never return while it is waiting for the callback to return. You must change the click handler to be async void and do a await instead of the .Result.

这篇关于ConfigureAwait(false)导致错误而不是死锁的情况的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆