让那些做面试官的屌丝lead不再抖脚系列(六)--- 众望所归的async/await

终于我们到最后了,从.net4.5开始出现的async/await关键字永远是各类面试以及开发的重点,这里先不详细介绍,在这篇中我们通过几个简单的例子来讲解:


1. 加了await关键字后是同步还是异步?

    这问题不好回答,但是如果调用的xxxAsync方法你不加await,那肯定是异步执行了,加了await后可以等待await后语句执行完,但是主线程并不是阻塞的,经常会有同学拿await和wait来比较,说await后会新开个线程,而wait只是单纯的阻塞线程,那我们来看个实践吧,这篇中所有的实践都在wpf中运行,至于为什么不用控制台,下一篇大家就知道了:

        private async Task Awaitest()
        {

            Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
        }
<pre name="code" class="csharp">        private async void AsyncButton_Click(object sender, RoutedEventArgs e)
        {
            Debug.WriteLine("Main" + Thread.CurrentThread.ManagedThreadId);
            await Awaitest();
        }


 点击button后运行结果为:Main99 

很简单的发现,其是并没有出现多线程什么的,线程一直没变,都在主线程上,所以并不存在同步异步的区分,而当我们把await Awaitest换成Awaittest.Wait()后也是一样的结果,并且未出现死锁,哎呦?怎么会死锁呀?这个我们稍后提,总之没有死锁,就说嘛其是线程根本没有变化。

2. 加了async关键字后就是异步了么?

   我们接着上面的代码,吧await关键字去掉,发现结果依旧是一样的,说明也没有异步,那么问题来了,怎么才会出现异步呢? 

   这里需要插播个广告,来介绍个Task.Delay(1000),这个看名字就知道是让线程等待,那和Thread.Sleep()有神马差别呢?

  很简单的例子,Thread.Sleep()是谁调用谁睡,就是主线程调用了,立马主线程会等10秒的样子,而Task.Delay是新开个线程去等10秒,

类似于

            Task.Factory.StartNew(() => 
            {
                Thread.Sleep(1000);
            });

3. async await组合起来才算异步么?

我们把上面的方法改变下:

        private async Task Awaitest()
        {
            Task.Factory.StartNew(() => 
            {
                Debug.WriteLine("subThread : " + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(1000);
            });
            //Task.Delay(5000);
            Debug.WriteLine("another : " +Thread.CurrentThread.ManagedThreadId);
        }

注释掉的Task.Delay是为了让大家看结果看的更清楚,这时候执行结果是

Main9
another : 9
subThread : 6

这很简单的变成了多线程,所以当我们在Task.Factory前添加了await后:

        private async Task Awaitest()
        {
            await Task.Factory.StartNew(() => 
            {
                Debug.WriteLine("subThread : " + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(1000);
            });
            //Task.Delay(5000);
            Debug.WriteLine("another : " +Thread.CurrentThread.ManagedThreadId);
        }


Main8
subThread : 9

咦?怎么就2个了?嗯,因为,死锁了呗~,我们先不管死锁的问题,现在的AwaitTest才真正变成了我们可以调用的一个异步方法,换句话说,他现在才有资格贴上xxxAsync的方法名。

好了关于死锁,这个问题国内的文档博客其实都没怎么写的很清楚,或许是我中文阅读能力比较差,又或许是我脑子没他们好用,

http://www.nikgupta.net/2014/07/configureawait-solves-deadlocks-when-using-tasks/

http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx

在这两篇文章中有了很详细的解释,英语还行的朋友可以去看一下,并不难的,但是之中也有点错误其是:

当我们把我们的awaitest方法写成:

        private async Task Awaitest()
        {
            
            var task =  Task.Factory.StartNew(() => 
            {
                Debug.WriteLine("subThread : " + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(1000);
            });
            var context = SynchronizationContext.Current;
            task.ContinueWith
                (
                 (t) => 
                 {
                     if (context == null)
                        Debug.WriteLine("another 1: " +Thread.CurrentThread.ManagedThreadId);
                    else
                        context.Post(delegate
                        { 
                            Debug.WriteLine("another 2: " +Thread.CurrentThread.ManagedThreadId);
                        }, null);
                   },TaskScheduler.Current
                );
            //Task.Delay(5000);
           
        }

我们会发现直接跑过去了,并没有起到我们想要的效果,而在上面两篇博客中都表示这是类同的的,当然可能我对他们similar的定义有所误解,可能他们只是想把await的过程写出来而已,确实,这里写成这样,就是写出了await和await后执行的步骤,当然不包括上来就是另起个线程的task,similar后的方法并没有等待里面的task,这就是为什么程序可以直接执行出来的原因,执行的结果:

Main 9
Main second 9
subThread : 10
another 2: 9

而我们会发现,task.ContinueWith后的方法已经回到了我们的主线程中,也就是ID=9的线程,因此这个例子想说明的是,在await方法执行完后,编译器会去寻找UI线程的context,也就是SynchronizationContext,然后把他捕捉到并且继续在UI线程上执行接下来的,其是和我们一开始说明的一样,只有在await后,方法才会进入异步,而离开了await,他又是同步了,所以需要找回UI线程。

那我们再来解释下为神马之前会死锁,其实就很容易了,xxx.Wait()会让context线程进入挂起等待,而在调用的方法中,await执行结束后会请求捕捉现在的UI线程,但是UI线程在等该方法执行完,于是进入了相互等待的节奏,然后就木有然后了。

这里引用一段国内开发者的解释,await xxx和xxx.wait()一样么?

“task.Wait()”是一个同步,可能阻塞的调用。它不会立刻返回到Wait()的调用者,直到这个任务进入最终状态,这意味着已进入RanToCompletion,Faulted,或Canceled完成状态。相比之下,”await task;”告诉编译器在”async”标记的方法内部插入一个隐藏的挂起/唤醒点,这样,如果等待的task没有完成,异步方法也会立马返回给调用者,当等待的任务完成时唤醒它从隐藏点处继续执行。当”await task;”会导致比较多应用程序无响应或死锁的情况下使用“task.Wait()”。

简单来说,await后编译器会自动切回目前的context,而wait()方法是被动等待,编译器并不会帮助他们干些什么。

那我们再来看一个有意思的情景,那编译器是什么时候自动获得了我们的ui context呢?

            var task = Task.Factory.StartNew(() => 
            {
                Debug.WriteLine("subThread : " + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(1000);
            });
            
            task.GetAwaiter().OnCompleted(() => {
                Debug.WriteLine("RunInCallBack : " + task.GetAwaiter().IsCompleted);
                Debug.WriteLine("AAThread : " + Thread.CurrentThread.ManagedThreadId);
            });
            Debug.WriteLine("RunBeforeCallBack : " + task.GetAwaiter().IsCompleted);
            

我们把上面的Awaittest方法改成这样,然后运行了看看,await本质上就是调用了个awaitale的对象,里面有OnComplete方法可以传入回掉函数,当然还有个IsComplete的值,我们看下结果:

Main 8
RunBeforeCallBack : False
Main second 8
subThread : 9
RunInCallBack : True
AAThread : 8

我们没有办法捕捉IsComplete什么时候改变的,但是我们可以推断,其实也就是肯定,当我们的task执行完了之后,task.GetAwaiter.IsComplete就会变成true,我们再加入个比较项,Task.IsComlete:既这个task什么时候打到RanToComplete状态呢:

var task = Task.Factory.StartNew(() => 
            {
                Debug.WriteLine("subThread : " + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(1000);
            });
            
            task.GetAwaiter().OnCompleted(() => {
                Debug.WriteLine("RunInCallBack : " + task.GetAwaiter().IsCompleted);
                Debug.WriteLine("AAThread : " + Thread.CurrentThread.ManagedThreadId);
                Debug.WriteLine("Task.IsComplete =  : " + task.IsCanceled);
            });
            Debug.WriteLine("Task.IsComplete =  : " + task.IsCanceled);
            Debug.WriteLine("RunBeforeCallBack : " + task.GetAwaiter().IsCompleted);

结果是Task.IsComlete 永远=false,别的结果我们就不贴了,所以这个也能说明这两个IsComlete是不一样的,当task执行完后,awaitable对象就被认为执行结束,这时候编译器就会回去寻找我们的context,而xx.wait()中,是需要整个task都到达RanToComlete状态时候,context才会睡醒,但是这之中其实已经有人要求他做事情了,因此就出现了死锁。

http://developer.51cto.com/art/201407/445556_all.htm

这篇帖子也有助于我们理解,但是可惜的是最后一点,awaitable的实质就是xxx.GetAwaitable()xxx的方法,这点是错的,xxx.GetAwaitable方法实质上就是wait(),他也会造成死锁,所以我们一般不推荐Task.Result直接获取方法的返回值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值