终于我们到最后了,从.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直接获取方法的返回值。