ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2020-02-06 15:50:54  阅读:175  来源: 互联网

标签:Core Task 异步 await 线程 context UI NET


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

        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

 

标签:Core,Task,异步,await,线程,context,UI,NET
来源: https://www.cnblogs.com/manupstairs/p/12268931.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有