await使用中的阻塞和并发
好吧,不加点陈述不让发首页。那我们来陈述一下本篇提到的问题和对应的方法。
在.NET4.5中,我们可以配合使用async和await两个关键字,来以写同步代码的方式,实现异步的操作。
好处我目前看来有两点:
1.不会阻塞UI线程。一旦UI线程不能及时响应,会极大的影响用户体验,这点在手机和平板的APP上尤为重要。
2.代码简洁。
- 相对基于event的异步方式,在多次回调的情况下(比如需要多次调web service,且后续调用基于前次调用的结果)特别明显。可以将多个+=Completed方法合并到一起。
- 相对于Begin/End的异步方式,避免了N重且不能对齐的大括号。
在同一个方法里存在多个await的情况下,如后续Async方法无需等待之前的Aysnc方法返回的结果,会隐式的以并行方式来运行后面的Async方法。
值得注意的是错误的写法会导致非预期的阻塞,下文会以简单的例子来讨论在使用await的情况下,怎样实现多个Task的并发执行。
我们这里先看几个方法定义:
static async Task Delay3000Async() { await Task.Delay(3000); Console.WriteLine(3000); Console.WriteLine(DateTime.Now); } static async Task Delay2000Async() { await Task.Delay(2000); Console.WriteLine(2000); Console.WriteLine(DateTime.Now); } static async Task Delay1000Async() { await Task.Delay(1000); Console.WriteLine(1000); Console.WriteLine(DateTime.Now); }
作用很简单,仅仅是起到延迟的作用。我们再来看如下写法的调用
Console.WriteLine(DateTime.Now); new Action(async () => { await Delay3000Async(); await Delay2000Async(); await Delay1000Async(); })();
结果如图,可以看出3个await是线性执行,第一个await会返回并阻止接下来的await后面的方法。这应该不是我们想要的效果,毕竟后面的方法并不依赖第一个方法的执行。
我们换一种写法,再运行一次程序:
var task3 = Delay3000Async(); var task2 = Delay2000Async(); var task1 = Delay1000Async(); new Action(async () => { await task3; await task2; await task1; })();
可以看到3个await后面的方法是并行执行的。MSDN的解释如下:
In an async method, tasks are started when they’re created. The Await (Visual Basic) or await (C#) operator is applied to the task at the point in the method where processing can’t continue until the task finishes.
However, you can separate creating the task from awaiting the task if your program has other work to accomplish that doesn’t depend on the completion of the task.
Between starting a task and awaiting it, you can start other tasks. The additional tasks implicitly run in parallel, but no additional threads are created.
到这里并没有结束 ,后面还有一些奇怪的事情:
var tasks = new List<Task> { Delay3000Async(), Delay2000Async(), Delay1000Async() }; tasks.ForEach(async _ => await _);
这个结果和上面是一样的,可以并行执行。这并不奇怪,我们仅仅是把Task放到一个List里,按照MSDN的说法,Task在被我们放进List时就被创建,且并发执行了。
那么我们再来一个List,这回放进去的不是Task,而是Func<Task>:
var funcList = new List<Func<Task>>() { Delay3000Async, Delay2000Async, Delay1000Async }; funcList.ForEach(async _ => await _());
仍然可以并发执行,看上去似乎没什么问题,但是作为Func<Task>来存储到List里,应该是没有被创建出来才对。为什么会能够并发呢?
我们再来看最后一组写法:
Func<Task> func3 = Delay3000Async; Func<Task> func2 = Delay2000Async; Func<Task> func1 = Delay1000Async; new Action(async () => { await func3(); await func2(); await func1(); } )();
意料之中的,以上的写法并不能够做到并发执行。而是需要按顺序执行func3,func2和func1。这很好解释,因为: a task is awaited as soon as it’s created。我们在创建Task之后立即就要求阻塞并等待完成才进行下一步。
写到这里的时候对List<Func<Task>>的例子开始迷糊了。参考了Reflector反编译的结果……我想说……没看出来有什么区别……本篇先到这里。一旦琢磨出个所以然,我再发第二篇好了。
还恭请各位高人不吝赐教,多多提点。
补充:对List<Func<Task>>的那个例子,我怀疑是Foreach这个扩展方法在偷偷做了优化。故增加了如下的试验:
static async Task TestForeach() { var funcList = new List<Func<Task>>() { Delay3000Async, Delay2000Async, Delay1000Async }; foreach (var item in funcList) {
//这里干了件蠢事,不要主动阻塞在这里,就可以并发了…… await item(); } }
试验结果表明用foreach来写的话,确实是做不到并行执行的。那么就需要去看一下Foreach的背后到底发生了什么。我还要研究研究才能写下一篇……
哈哈哈哈,干了件蠢事情……