• .NET Core学习笔记(4)——谨慎混合同步和异步代码


    原则上我们应该避免编写混合同步和异步的代码,这其中最大的问题就是很容易出现死锁。让我们来看下面的例子:

            private void ButtonDelayBlock_Click(object sender, RoutedEventArgs e)
            {
                Delay100msAsync().Wait();
                this.buttonDelayBlock.Content = "Done";
            }
    
            private async Task Delay100msAsync()
            {
                await Task.Delay(100);
            }

    这段代码取自Sample代码中的AsyncBlockSample工程,一个简单的WPF程序(.NET Core)。

    https://github.com/manupstairs/AsyncAwaitPractice

    在buttonDelayBlock按钮被点击后,会执行Dealy100msAsync方法,同时我们希望将该异步方法以同步的方式运行。在该方法完成后,将buttonDelayBlock按钮的文字设置为“Done”。

    所以我们没有对Delay100msAsync方法应用await关键字,而是通过Wait方法同步等待执行结果。

    非常遗憾在WPF之类的GUI程序中,我们点击buttonDelayBlock按钮后,程序将会进入死锁的状态。

    这是因为一个未完成的Task被await时(Task.Delay(100)返回的Task),将捕获当前的context,用于Task完成时恢复执行接下来的操作。在GUI程序中,此时的context是当前 SynchronizationContext,而GUI程序中的 SynchronizationContext同一时间只能运行一个线程(在Sample里是UI线程)。

    所以当Task.Delay(100)完成时,希望能够回到UI线程接着执行,但UI线程正通过Delay100msAsync.Wait()方法在等待Task完成。这踏马就跟吵架了都在等对方先低头,整个程序都不好了,然后就死了……

    值得一提的是Console程序并不会出现上述死锁,这是因为Console程序中的SynchronizationContext可以通过ThreadPool来调度不同线程来完成Task,而不会像GUI程序卡在UI线程进退不得。这样不同的迷惑行为,即使是十分年长的猿类也瑟瑟发抖……

    最理想的情况就是只编写异步代码。问题是除非编写UWP这样,从底层API调用就强制异步。不然很难避免旧有的同步API的使用。

    更不用说成千上万的旧有代码的维护,迁移桌面程序到MS Store,已有GUI程序Win10 style化需求等等。混合同步和异步代码实在是难以避免的。像例子中需要等待异步方法完成,再根据结果执行的情况就更常见了。

    解决上述死锁的一个方式是通过ConfigureAwait方法来配置context。

    async Task MyMethodAsync()
    {
      // Code here runs in the original context.
        await Task.Delay(1000);
      // Code here runs in the original context.
        await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
      // Code here runs without the original
      // context (in this case, on the thread pool).
    }

    如注释所描述,第一个await Task.Delay方法前后的代码块会在相同的context中执行,因为Task完成后仍会返回原先的context。而第二个await Task.Delay则不再依赖原先的context。如果是在GUI程序中执行上面的代码,后续的代码将在ThreadPool,而不是之前的UI线程上执行。

    在这种情况下如果出现了对UI元素的操作,便会出现祖传的跨线程操作Exception。

    我们回到死锁的问题上,通过ConfigureAwait配置context的代码如下:

    private async Task Delay100msWithoutContextAsync()
    {
        await Task.Delay(100).ConfigureAwait(false);
    }
    private void ButtonDelay_Click(object sender, RoutedEventArgs e)
    {
        Delay100msWithoutContextAsync().Wait();
        this.buttonDelay.Content = "Done";
    }

    我们可以通过这种方式终结异步代码链的传递,将一小块的异步代码隐匿在旧有的同步代码中使用,当然仍需要十分小心。

    这里还有一种略显繁琐且奇怪的方式来解决死锁问题:

            private void ButtonDelay2_Click(object sender, RoutedEventArgs e)
            {
                var text = buttonDelay2.Content.ToString();
                var length = Task.Run(async () => { return await GetLengthAsync(text); }).Result;
                buttonDelay2.Content = $"Total length is {length}";
            }
    
            private async Task<int> GetLengthAsync(string text)
            {
                await Task.Delay(3000);
                return text.Length;
            }

    异步方法GetLengthAsync能返回传入字符串的长度,Task.Run(…)会通过ThreadPool来异步地执行一个Func<Task<int>>,且返回Task<int>,而Task<int>.Result属性又以同步的方式阻塞在这里等待结果。

    与之前Wait最大的不同,是因为Task.Run利用了ThreadPool没有导致UI线程的死锁。

    我们再回到通过ConfigureAwait配置context,等待异步方法结果的方式:

            private void ButtonDelay3_Click(object sender, RoutedEventArgs e)
            {
                var text = buttonDelay3.Content.ToString();
                var length = GetLengthWithoutContextAsync(text).Result;
                buttonDelay3.Content = $"Button 3 total length is {length}";
            }
    
            private async Task<int> GetLengthWithoutContextAsync(string text)
            {
                await Task.Delay(3000).ConfigureAwait(false);
                //Cannot access UI thead here, will throw exception
                //buttonDelay3.Content = $"Try to access UI thread";
                return text.Length;
            }

    同样是等待Task<type>的Result,相对而言更推荐这种方式,结构清晰且更好理解。注释提到ConfigureAwait(false)之后的代码是不能访问UI线程的。

    本篇讨论了混合同步和异步代码时的一些注意事项,还请各位大佬斧正。

    Github:

    https://github.com/manupstairs/AsyncAwaitPractice

  • 相关阅读:
    SSH的密钥登录配置
    VMware 15pro虚拟机网络设置
    12种SQL注入报错方式
    PHP myadmin 无路径getshell
    MySQL数据库基本操作
    ubuntu 16.04安装后的简单优化
    无聊中,静思自己。
    Silverlight 4.0+Linq to sql 网站后台登陆功能(一)
    AspNetPager和Linq to sql 的完美结合
    Linq to sql 的DataContext 持久化层写法
  • 原文地址:https://www.cnblogs.com/manupstairs/p/12268931.html
Copyright © 2020-2023  润新知