0%

探索c#之Async、Await剖析 - 蘑菇先生 - 博客园

Excerpt

Async,主线程A逻辑->异步任务线程B逻辑->主线程C逻辑。
注意:这3个步骤是有可能会使用同一个线程的,也可能会使用2个,甚至3个线程。

  1. net4.5的async,抛去语法糖就是Net4.0的Task+状态机。
  2. net4.0的Task, 退化到3.5即是(Thread、Th

阅读目录:

  1. 基本介绍
  2. 基本原理剖析
  3. 内部实现剖析
  4. 重点注意的地方
  5. 总结

基本介绍

Async、Await是net4.x新增的异步编程方式,其目的是为了简化异步程序编写,和之前APM方式简单对比如下。

APM方式,BeginGetRequestStream需要传入回调函数,线程碰到BeginXXX时会以非阻塞形式继续执行下面逻辑,完成后回调先前传入的函数。

1
2
3
HttpWebRequest myReq =(HttpWebRequest)WebRequest.Create(<span>"</span><span>http://cnblogs.com/</span><span>"</span><span>);
myReq.BeginGetRequestStream();
</span><span>//</span><span>to do</span>

Async方式,使用Async标记Async1为异步方法,用Await标记GetRequestStreamAsync表示方法内需要耗时的操作。主线程碰到await时会立即返回,继续以非阻塞形式执行主线程下面的逻辑。当await耗时操作完成时,继续执行Async1下面的逻辑

复制代码

1
2
3
4
5
6
<span>static</span> <span>async</span> <span>void</span><span> Async1()
{
HttpWebRequest myReq </span>= (HttpWebRequest)WebRequest.Create(<span>"</span><span>http://cnblogs.com/</span><span>"</span><span>);
</span><span>await</span><span> myReq.GetRequestStreamAsync();
</span><span>//</span><span>to do</span>
}

复制代码

上面是net类库实现的异步,如果要实现自己方法异步。
APM方式:

1
2
3
<span>public</span> <span>delegate</span> <span>int</span> MyDelegate(<span>int</span><span> x);   
MyDelegate mathDel </span>= <span>new</span> MyDelegate((a) =&gt; { <span>return</span> <span>1</span><span>; });
mathDel.BeginInvoke(</span><span>1</span>, (a) =&gt; { },<span>null</span>);

Async方式:

复制代码

1
2
3
4
5
6
7
<span>static</span> <span>async</span> <span>void</span><span> Async2()
{
</span><span>await</span> Task.Run(() =&gt; { Thread.Sleep(<span>500</span>); Console.WriteLine(<span>"</span><span>bbb</span><span>"</span><span>); });
Console.WriteLine(</span><span>"</span><span>ccc</span><span>"</span><span>);
}
Async2();
Console.WriteLine(</span><span>"</span><span>aaa</span><span>"</span>);

复制代码

对比下来发现,async/await是非常简洁优美的,需要写的代码量更少,更符合人们编写习惯。
因为人的思维对线性步骤比较好理解的。

APM异步回调的执行步骤是:A逻辑->假C回调逻辑->B逻辑->真C回调逻辑,这会在一定程度造成思维的混乱,当一个项目中出现大量的异步回调时,就会变的难以维护。
Async、Await的加入让原先这种混乱的步骤,重新拨正了,执行步骤是:A逻辑->B逻辑->C逻辑。

基本原理剖析

作为一个程序员的自我修养,刨根问底的好奇心是非常重要的。 Async刚出来时会让人有一头雾水的感觉,await怎么就直接返回了,微软怎么又出一套新的异步模型。那是因为习惯了之前的APM非线性方式导致的,现在重归线性步骤反而不好理解。 学习Async时候,可以利用已有的APM方式去理解,以下代码纯属虚构
比如把Async2方法想象APM方式的Async3方法:

复制代码

1
2
3
4
5
6
7
8
9
<span>static</span> <span>async</span> <span>void</span><span> Async3()
{
</span><span>var</span> task= <span>await</span> Task.Run(() =&gt; { Thread.Sleep(<span>500</span>); Console.WriteLine(<span>"</span><span>bbb</span><span>"</span><span>); });
</span><span>//</span><span>注册task完成后回调</span>
task.RegisterCompletedCallBack(() =&gt;<span>
{
Console.WriteLine(</span><span>"</span><span>ccc</span><span>"</span><span>);
});
}</span>

复制代码

上面看其来就比较好理解些的,再把Async3方法想象Async4方法:

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<span>static</span>  <span>void</span><span> Async4()
{
</span><span>var</span> thread = <span>new</span> Thread(() =&gt;<span>
{
Thread.Sleep(</span><span>500</span><span>);
Console.WriteLine(</span><span>"</span><span>bbb</span><span>"</span><span>);
});
</span><span>//</span><span>注册thread完成后回调</span>
thread.RegisterCompletedCallBack(() =&gt;<span>
{
Console.WriteLine(</span><span>"</span><span>ccc</span><span>"</span><span>);
});
thread.Start();
}</span>

复制代码

这样看起来就非常简单明了,连async都去掉了,变成之前熟悉的编程习惯。虽然代码纯属虚构,但基本思想是相通的,差别在于实现细节上面。

内部实现剖析

作为一个程序员的自我修养,严谨更是不可少的态度。上面的基本思想虽然好理解了,但具体细节呢,编程是个来不得半点虚假的工作,那虚构的代码完全对不住看官们啊。

继续看Async2方法,反编译后的完整代码如下:

View Code

发现async、await不见了,原来又是编译器级别提供的语法糖优化,所以说async不算是全新的异步模型。 可以理解为async更多的是线性执行步骤的一种回归,专门用来简化异步代码编写。
从反编译后的代码看出编译器新生成一个继承IAsyncStateMachine 的状态机结构asyncd(代码中叫d__2,后面简写AsyncD),下面是基于反编译后的代码来分析的

IAsyncStateMachine最基本的状态机接口定义:

1
2
3
4
5
<span>public</span> <span>interface</span><span> IAsyncStateMachine
{
</span><span>void</span><span> MoveNext();
</span><span>void</span><span> SetStateMachine(IAsyncStateMachine stateMachine);
}</span>

既然没有了async、await语法糖的阻碍,就可以把代码执行流程按线性顺序来理解,其整个执行步骤如下:

1. 主线程调用Async2()方法
2. Async2()方法内初始化状态机状态为-1,启动AsyncD
3. MoveNext方法内部开始执行,其task.run函数是把任务扔到线程池里,返回个可等待的任务句柄。MoveNext源码剖析:

//要执行任务的委托

1
Program.CS$&lt;&gt;9__CachedAnonymousMethodDelegate1 = <span>new</span> Action(Program.&lt;Async2&gt;b__0);

//开始使用task做异步,是net4.0基于任务task的编程方式。

1
awaiter =Task.Run(Program.CS$&lt;&gt;9__CachedAnonymousMethodDelegate1).GetAwaiter();

//设置状态为0,以便再次MoveNext直接break,执行switch后面的逻辑,典型的状态机模式。

//返回调用async2方法的线程,让其继续执行主线程后面的逻辑

1
2
<span>this</span>.&lt;&gt;t__builder.AwaitUnsafeOnCompleted&lt;TaskAwaiter, Program.&lt;Async2&gt;d__2&gt;(<span>ref</span> awaiter, <span>ref</span> <span>this</span><span>);
</span><span>return</span>;

4. 这时就已经有2个线程在跑了,分别是主线程和Task.Run在跑的任务线程。

5. 执行主线程后面逻辑输出aaa,任务线程运行完成后输出bbb、在继续执行任务线程后面的业务逻辑输出ccc。

1
2
3
4
<span>Label_0090: 
awaiter.GetResult();
awaiter </span>= <span>new</span><span> TaskAwaiter();
Console.WriteLine(</span><span>"</span><span>ccc</span><span>"</span>);

这里可以理解为async把整个主线程同步逻辑,分拆成二块。 第一块是在主线程直接执行,第二块是在任务线程完成后执行, 二块中间是任务线程在跑,其源码中awaiter.GetResult()就是在等待任务线程完成后去执行第二块。
从使用者角度来看执行步骤即为: 主线程A逻辑->异步任务线程B逻辑->主线程C逻辑。

复制代码

1
2
3
4
5
6
7
<span>        Test();
Console.WriteLine(</span><span>"</span><span>A逻辑</span><span>"</span><span>);
</span><span>static</span> <span>async</span> <span>void</span><span> Test()
{
</span><span>await</span> Task.Run(() =&gt; { Thread.Sleep(<span>1000</span>); Console.WriteLine(<span>"</span><span>B逻辑</span><span>"</span><span>); });
Console.WriteLine(</span><span>"</span><span>C逻辑</span><span>"</span><span>);
}</span>

复制代码

回过头来对比下基本原理剖析小节中的虚构方法Async4(),发现区别在于一个是完成后回调,一个是等待完成后再执行,这也是实现异步最基本的两大类方式。

重点注意的地方

主线程A逻辑->异步任务线程B逻辑->主线程C逻辑。

注意:这3个步骤是有可能会使用同一个线程的,也可能会使用2个,甚至3个线程。 可以用Thread.CurrentThread.ManagedThreadId测试下得知。

复制代码

1
2
3
4
5
6
7
8
9
10
<span>     Async7();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
</span><span>static</span> <span>async</span> <span>void</span><span> Async7()
{
</span><span>await</span> Task.Run(() =&gt;<span>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
});
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}</span>

复制代码

正由于此,才会有言论说Async不用开线程,也有说需要开线程的,从单一方面来讲都是对的,也都是错的。 上面源码是从简分析的,具体async内部会涉及到线程上下文切换,线程复用、调度等。 想深入的同学可以研究下ExecutionContextSwitcher、 SecurityContext.RestoreCurrentWI、ExecutionContext这几个东东。

其实具体的物理线程细节可以不用太关心,知道其【主线程A逻辑->异步任务线程B逻辑->主线程C逻辑】这个基本原理即可。 另外Async也会有线程开销的,所以要合理分业务场景去使用。

总结

从逐渐剖析Async中发现,Net提供的异步方式基本上一脉相承的,如:
1. net4.5的Async,抛去语法糖就是Net4.0的Task+状态机。
2. net4.0的Task, 退化到3.5即是(Thread、ThreadPool)+实现的等待、取消等API操作。

本文以async为起点,简单剖析了其内部原理及实现,希望对大家有所帮助。