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

时间:2021-11-01 15:31:43

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

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:

我遵循了在我的库中使用configurewait (false)(如本文中所述)的建议。然后我从我的测试应用中同步使用它,得到一个失败:

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

The failure is:

失败是:

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

WpfApplication1.vshost。exe错误:0:系统。InvalidOperationException:调用线程不能访问这个对象,因为不同的线程拥有它。在System.Windows.Threading.Dispatcher.VerifyAccess System.Windows.DependencyObject()。SetValue(依赖于属性dp,对象值)位于System.Windows.Controls.ContentControl。在WpfApplication1.MainWindow set_Content(对象值)。ActionToProcessNewValue(String s)在C:\开发\ \四\ WpfApplication1 \ WpfApplication1 \ MainWindow.xaml测试。dc: MyLibrary1.ClassFromMyLibrary1.d__0.MoveNext() C:\dev\ dev\测试4\WpfApplication1\WpfApplication1\ WpfApplication1\MainWindow.xaml。cs:第77行--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- -在系统。在System.Runtime.CompilerServices.TaskAwaiter ThrowForNonSuccess(工作任务)。HandleNonSuccessAndDebuggerNotification(任务)在system . runtime. compiler services . taskawaierservices . taskawaiter 1.GetResult() at WpfApplication1. mainwindow .d__1. movenext () C:\dev\ dev tests 4\WpfApplication1\WpfApplication1\ WpfApplication1\MainWindow.xaml。cs:第39行异常抛出:'系统。WpfApplication1.exe InvalidOperationException”

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

很明显,错误发生是因为我的库中的服务员丢弃了当前的WPF上下文。

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

另一方面,在删除库中各处的configurewait (false)之后,我显然会得到一个死锁。

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

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

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, 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.

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

3 个解决方案

#1


3  

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.

通常,如果回调被保证在调用它的同一线程上,那么库会记录,如果没有记录,最安全的选项将是假设它没有记录。您的代码示例(以及您正在处理的第三方,从您的评论中我可以看出)属于“不保证”类别。在这种情况下,您只需要检查是否需要从回调方法中进行调用并进行调用,您可以调用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);
}

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

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.

这里是另一个版本,如果您想从Label1获得值。内容而不是分配它,这也不需要在回调中使用async/ wait。

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

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.

注意:如果不删除. result按钮单击处理程序,即Dispatcher,那么所有这些方法都会导致程序死锁。调用或调度程序。回调中的InvokeAsync在等待. result返回时永远不会启动,而. result在等待回调返回时永远不会返回。您必须将单击处理程序更改为async void,并执行一个wait而不是. result。

#2


3  

Actually, you're receiving a callback in your ClassFromMyLibrary1 and you can't assume what it'll do (like updating a Label). You don't need ConfigureAwait(false) in your class library, as the same link you provided gives us an explanation like this:

实际上,您正在从ClassFromMyLibrary1中接收回调,您不能假设它会做什么(比如更新标签)。在类库中不需要configure rewait (false),因为您提供的链接提供了如下解释:

As asynchronous GUI applications grow larger, you might find many small parts of async methods all using the GUI thread as their context. This can cause sluggishness as responsiveness suffers from "thousands of paper cuts".

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

To mitigate this, await the result of ConfigureAwait whenever you can.

为了减轻这种情况,尽可能等待configure等待的结果。

By using ConfigureAwait, you enable a small amount of parallelism: Some asynchronous code can run in parallel with the GUI thread instead of constantly badgering it with bits of work to do.

通过使用configurewait,您可以启用少量的并行性:一些异步代码可以与GUI线程并行运行,而不是不断地用一些工作来干扰它。

Now take a read here:

现在读一下:

You should not use ConfigureAwait when you have code after the await in the method that needs the context. For GUI apps, this includes any code that manipulates GUI elements, writes data-bound properties or depends on a GUI-specific type such as Dispatcher/CoreDispatcher.

当您在需要上下文的方法中有了等待之后的代码时,不应该使用configure rewait。对于GUI应用程序,这包括任何操作GUI元素的代码,写入数据绑定属性,或者依赖于特定于GUI的类型,例如Dispatcher/CoreDispatcher。

You're doing exactly the opposite. You're trying to update GUI in two points, one in your callback method, and another here:

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

var c = new ClassFromMyLibrary1();

var v1 = c.MethodFromMyLibrary1("test1", ActionToProcessNewValue).Result;

Label2.Content = v1; // updating GUI...

That's why removing ConfigureAwait(false) solves your problem. Also, you can make your button click handler async and await your ClassFromMyLibrary1 method call.

这就是为什么删除configurewaiting (false)可以解决您的问题。此外,您可以让您的按钮单击handler并等待ClassFromMyLibrary1方法调用。

#3


1  

In my opinion, you should redesign your library API to not mix a callback-based API with a Task-based API. At least in your example code there's no compelling case to be made to do that and you've nailed one reason not do do that - it is hard to control the context in which your callback runs.

在我看来,您应该重新设计您的库API,以避免将基于调用的API与基于任务的API混合在一起。至少在您的示例代码中,没有必要这样做,而且您已经确定了不这样做的一个原因——很难控制回调运行的上下文。

I'd change your library API to be like so:

我将您的库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);
        }
    }
}

And call it like so:

这样称呼它:

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

#1


3  

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.

通常,如果回调被保证在调用它的同一线程上,那么库会记录,如果没有记录,最安全的选项将是假设它没有记录。您的代码示例(以及您正在处理的第三方,从您的评论中我可以看出)属于“不保证”类别。在这种情况下,您只需要检查是否需要从回调方法中进行调用并进行调用,您可以调用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);
}

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

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.

这里是另一个版本,如果您想从Label1获得值。内容而不是分配它,这也不需要在回调中使用async/ wait。

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

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.

注意:如果不删除. result按钮单击处理程序,即Dispatcher,那么所有这些方法都会导致程序死锁。调用或调度程序。回调中的InvokeAsync在等待. result返回时永远不会启动,而. result在等待回调返回时永远不会返回。您必须将单击处理程序更改为async void,并执行一个wait而不是. result。

#2


3  

Actually, you're receiving a callback in your ClassFromMyLibrary1 and you can't assume what it'll do (like updating a Label). You don't need ConfigureAwait(false) in your class library, as the same link you provided gives us an explanation like this:

实际上,您正在从ClassFromMyLibrary1中接收回调,您不能假设它会做什么(比如更新标签)。在类库中不需要configure rewait (false),因为您提供的链接提供了如下解释:

As asynchronous GUI applications grow larger, you might find many small parts of async methods all using the GUI thread as their context. This can cause sluggishness as responsiveness suffers from "thousands of paper cuts".

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

To mitigate this, await the result of ConfigureAwait whenever you can.

为了减轻这种情况,尽可能等待configure等待的结果。

By using ConfigureAwait, you enable a small amount of parallelism: Some asynchronous code can run in parallel with the GUI thread instead of constantly badgering it with bits of work to do.

通过使用configurewait,您可以启用少量的并行性:一些异步代码可以与GUI线程并行运行,而不是不断地用一些工作来干扰它。

Now take a read here:

现在读一下:

You should not use ConfigureAwait when you have code after the await in the method that needs the context. For GUI apps, this includes any code that manipulates GUI elements, writes data-bound properties or depends on a GUI-specific type such as Dispatcher/CoreDispatcher.

当您在需要上下文的方法中有了等待之后的代码时,不应该使用configure rewait。对于GUI应用程序,这包括任何操作GUI元素的代码,写入数据绑定属性,或者依赖于特定于GUI的类型,例如Dispatcher/CoreDispatcher。

You're doing exactly the opposite. You're trying to update GUI in two points, one in your callback method, and another here:

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

var c = new ClassFromMyLibrary1();

var v1 = c.MethodFromMyLibrary1("test1", ActionToProcessNewValue).Result;

Label2.Content = v1; // updating GUI...

That's why removing ConfigureAwait(false) solves your problem. Also, you can make your button click handler async and await your ClassFromMyLibrary1 method call.

这就是为什么删除configurewaiting (false)可以解决您的问题。此外,您可以让您的按钮单击handler并等待ClassFromMyLibrary1方法调用。

#3


1  

In my opinion, you should redesign your library API to not mix a callback-based API with a Task-based API. At least in your example code there's no compelling case to be made to do that and you've nailed one reason not do do that - it is hard to control the context in which your callback runs.

在我看来,您应该重新设计您的库API,以避免将基于调用的API与基于任务的API混合在一起。至少在您的示例代码中,没有必要这样做,而且您已经确定了不这样做的一个原因——很难控制回调运行的上下文。

I'd change your library API to be like so:

我将您的库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);
        }
    }
}

And call it like so:

这样称呼它:

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