一、难以被接受的async

自从C#5.0,语法糖大家庭又加入了两位新成员: async和await。

然而从我知道这两个家伙之后的很长一段时间,我甚至都没搞明白应该怎么使用它们,这种全新的异步编程模式对于习惯了传统模式的人来说实在是有些难以接受,不难想象有多少人仍然在使用手工回调委托的方式来进行异步编程。
C#中的语法糖非常多,从自动属性到lock、using,感觉都很好理解很容易就接受了,为什么偏偏async和await就这么让人又爱又恨呢?

我想,不是因为它不好用(相反,理解了它们之后是非常实用又易用的),而是因为它来得太迟了!
传统的异步编程在各种语言各种平台前端后端差不多都是同一种模式,给异步请求传递一个回调函数,回调函数中再对响应进行处理,发起异步请求的地方对于返回值是一无所知的。我们早就习惯了这样的模式,即使这种模式十分蹩脚。
而async和await则打破了请求发起与响应接收之间的壁垒,让整个处理的逻辑不再跳过来跳过去,成为了完全的线性流程!线性才是人脑最容易理解的模式!

广告时间:
[C#]async和await刨根问底
这篇随笔把本文未解决的问题都搞定了,并且对async和await的总体面貌做了最终总结,对调查过程没有兴趣希望直接看结果的可以直接戳进去~

二、理解async,谁被异步了

如果对于Java有一定认识,看到async的使用方法应该会觉得有些眼熟吧?

//Java
synchronized void sampleMethod() { }
// C#
async void SampleMethod() { }

说到这里我想对MS表示万分的感谢,幸好MS的设计师采用的简写而不是全拼,不然在没有IDE的时候(比如写上面这两个示例的时候)我不知道得检查多少次有没有拼错同步或者异步的单词。。。
Java中的synchronized关键字用于标识一个同步块,类似C#的lock,但是synchronized可以用于修饰整个方法块。
而C#中async的作用就是正好相反的了,它是用于标识一个异步方法。
同步块很好理解,多个线程不能同时进入这一区块,就是同步块。而异步块这个新东西就得重新理解一番了。

先看看async到底被编译成了什么吧:

 .method private hidebysig
instance void SampleMethod () cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
1f 2e 6f 6d 2b
3c 6d 6c 4d 6f 3e 5f
5f
)
.custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( )
// Method begins at RVA 0x20b0
// Code size 46 (0x2e)
.maxstack
.locals init (
[] valuetype Test.Program/'<SampleMethod>d__0',
[] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder
) IL_0000: ldloca.s
IL_0002: ldarg.0
IL_0003: stfld class Test.Program Test.Program/'<SampleMethod>d__0'::'<>4__this'
IL_0008: ldloca.s
IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Create()
IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder Test.Program/'<SampleMethod>d__0'::'<>t__builder'
IL_0014: ldloca.s
IL_0016: ldc.i4.m1
IL_0017: stfld int32 Test.Program/'<SampleMethod>d__0'::'<>1__state'
IL_001c: ldloca.s
IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder Test.Program/'<SampleMethod>d__0'::'<>t__builder'
IL_0023: stloc.1
IL_0024: ldloca.s
IL_0026: ldloca.s
IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Start<valuetype Test.Program/'<SampleMethod>d__0'>(!!&)
IL_002d: ret
} // end of method Program::SampleMethod

不管你们吓没吓到,反正我第一次看到是吓了一大跳。。。之前的空方法SampleMethod被编译成了这么一大段玩意。
另外还生成了一个名叫'<SampleMethod>d__0'的内部结构体,整个Program类的结构就像这样:

其他的暂时不管,先尝试把上面这段IL还原为C#代码:

 void SampleMethod()
{
'<SampleMethod>d__0' local0;
AsyncVoidMethodBuilder local1; local0.'<>4_this' = this;
local0.'<>t__builder' = AsyncVoidMethodBuilder.Create();
local0.'<>1_state' = -; local1 = local0.'<>t__builder';
local1.Start(ref local0);
}

跟进看Start方法:

 // System.Runtime.CompilerServices.AsyncVoidMethodBuilder
[__DynamicallyInvokable, DebuggerStepThrough]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
this.m_coreState.Start<TStateMachine>(ref stateMachine);
}

继续跟进:

 // System.Runtime.CompilerServices.AsyncMethodBuilderCore
[DebuggerStepThrough, SecuritySafeCritical]
internal void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
if (stateMachine == null)
{
throw new ArgumentNullException("stateMachine");
}
Thread currentThread = Thread.CurrentThread;
ExecutionContextSwitcher executionContextSwitcher = default(ExecutionContextSwitcher);
RuntimeHelpers.PrepareConstrainedRegions();
try
{
ExecutionContext.EstablishCopyOnWriteScope(currentThread, false, ref executionContextSwitcher);
stateMachine.MoveNext();
}
finally
{
executionContextSwitcher.Undo(currentThread);
}
}

注意到上面黄底色的stateMachine就是自动生成的内部结构体'<SampleMethod>d__0',再看看自动生成的MoveNext方法,IL就省了吧,直接上C#代码:

 void MoveNext()
{
bool local0;
Exception local1; try
{
local0 = true;
}
catch (Exception e)
{
local1 = e;
this.'<>1__state' = -;
this.'<>t__builder'.SetException(local1);
return;
} this.'<>1__state' = -;
this.'<>t__builder'.SetResult()
}

因为示例是返回void的空方法,所以啥也看不出来,如果在方法里头稍微加一点东西,比如这样:

async void SampleMethod()
{
Thread.Sleep();
Console.WriteLine("HERE");
}

然后再看看SampleMethod的IL:

 .method private hidebysig
instance void SampleMethod () cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
1f 2e 6f 6d 2b
3c 6d 6c 4d 6f 3e 5f
5f
)
.custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( )
// Method begins at RVA 0x20bc
// Code size 46 (0x2e)
.maxstack
.locals init (
[] valuetype Test.Program/'<SampleMethod>d__0',
[] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder
) IL_0000: ldloca.s
IL_0002: ldarg.0
IL_0003: stfld class Test.Program Test.Program/'<SampleMethod>d__0'::'<>4__this'
IL_0008: ldloca.s
IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Create()
IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder Test.Program/'<SampleMethod>d__0'::'<>t__builder'
IL_0014: ldloca.s
IL_0016: ldc.i4.m1
IL_0017: stfld int32 Test.Program/'<SampleMethod>d__0'::'<>1__state'
IL_001c: ldloca.s
IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder Test.Program/'<SampleMethod>d__0'::'<>t__builder'
IL_0023: stloc.1
IL_0024: ldloca.s
IL_0026: ldloca.s
IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Start<valuetype Test.Program/'<SampleMethod>d__0'>(!!&)
IL_002d: ret
} // end of method Program::SampleMethod

看出来什么变化了吗?????看不出来就对了,因为啥都没变。
那追加的代码跑哪去了?!在这呢:

 void MoveNext()
{
bool local0;
Exception local1; try
{
local0 = true;
Thread.Sleep();
Console.WriteLine("HERE");
}
catch (Exception e)
{
local1 = e;
this.'<>1__state' = -;
this.'<>t__builder'.SetException(local1);
return;
} this.'<>1__state' = -;
this.'<>t__builder'.SetResult()
}

至今为止都没看到异步在哪发生,因为事实上一直到现在确实都是同步过程。Main方法里这么写:

static void Main(string[] args)
{
new Program().SampleMethod();
Console.WriteLine("THERE");
Console.Read();
}

运行结果是这样的:

HERE
THERE

"THERE"被"HERE"阻塞了,并没有异步先行。
虽然到此为止还没看到异步发生,但是我们可以得出一个结论:
async不会导致异步

到底怎么才能异步?还是得有多个线程才能异步嘛,是时候引入Task了:

async void SampleMethod()
{
Task.Run(() =>
{
Thread.Sleep();
Console.WriteLine("HERE");
});
}

Main方法不变,运行结果是这样的:

THERE
HERE

当然,把SampleMethod前头的async去掉也可以得到同样的结果。。。
所以async貌似是个鸡肋啊?然而并不是这样的!

三、理解await,是谁在等

继续改造上面的SampleMethod,不过现在还得加一个GetHere的方法了:

async void SampleMethod()
{
Console.WriteLine(await GetHere());
} Task<string> GetHere()
{
return Task.Run(() =>
{
Thread.Sleep();
return "HERE";
});
}

Main方法仍然不变,运行结果也没有变化。但是现在就不能去掉async了,因为没有async的方法里头不允许await!
首先要注意的是,GetHere方法的返回值是Task<string>,而从运行结果可以看出来WriteLine的重载版本是string参数,至于为什么,之后再看。
这一次的结论很容易就得出了,很明显主线程没有等SampleMethod返回就继续往下走了,而调用WriteLine的线程则必须等到"HERE"返回才能接收到实参。
那么,WriteLine又是哪个线程调用的?
这一次可以轻车熟路直接找MoveNext方法了。需要注意的是,现在Program类里头已经变成了这副德性:

这个时候try块里头的IL已经膨胀到了50行。。。还原为C#后如下:

 bool '<>t__doFinallyBodies';
Exception '<>t__ex';
int CS$$;
TaskAwaiter<string> CS$$;
TaskAwaiter<string> CS$$; try
{
'<>t__doFinallyBodies' = true;
CS$$ = this.'<>1__state';
if (CS$$ != )
{
CS$$ = this.'<>4__this'.GetHere().GetAwaiter();
if (!CS$$.IsCompleted)
{
this.'<>1__state' = ;
this.'<>u__$awaiter1' = CS$$;
this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$$, ref this);
'<>t__doFinallyBodies' = false;
return;
}
}
else
{
CS$$ = this.'<>u__$awaiter1';
this.'<>u__$awaiter1' = CS$$;
this.'<>1__state' = -;
} Console.WriteLine(CS$$.GetResult());
}

貌似WriteLine仍然是主线程调用的?!苦苦等待返回值的难道还是主线程?!

四、异步如何出现

感觉越看越奇怪了,既然主线程没有等SampleMethod返回,但是主线程又得等到GetResult返回,那么异步到底是怎么出现的呢?
注意到第20行的return,主线程跑进了这一行自然就直接返回了,从而不会发生阻塞。
那么新的问题又来了,既然MoveNext在第20行就直接return了,谁来再次调用MoveNext并走到第30行?

MoveNext方法是实现自IAsyncStateMachine接口,借助于ILSpy的代码解析,找到了三个调用方:

第一个是之前看到的,SampleMethod内部调用到的方法,后两个是接下来需要跟踪的目标。
调试模式跟到AsyncMethodBuilderCore的内部,然后在InvokeMoveNext和Run方法的首行打断点,设置命中条件为打印默认消息并继续执行。
最后在Main函数和lambda表达式的首行也打上同样的断点并设置打印消息。F5执行,然后可以在即时窗口中看到如下信息:

Function: Test.Program.Main(string[]), Thread: 0xE88 主线程
Function: Test.Program.GetHere.AnonymousMethod__3(), Thread: 0x37DC 工作线程
Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run(), Thread: 0x37DC 工作线程
Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object), Thread: 0x37DC 工作线程

这样至少弄明白了一点,"HERE"是由另一个工作线程返回的。
看不明白的是,为什么lambda的执行在两次MoveNext被调用之前。。。从调用堆栈也得到有用的信息,这个问题以后有空再深究吧。。。

五、Task<TResult> to TResult

正如之前所说,GetHere方法的返回值是Task<string>,WriteLine接收的实参是string,这是怎么做到的呢?
关键当然就是调用GetHere时候用的await了,如果去掉await,就会看到这样的结果:

System.Threading.Tasks.Task`[System.String]
THERE

这一次GetHere的返回又跑到"THERE"的前头了,因为没有await就没有阻塞,同时GetHere的本质也暴露了,返回值确确实实就是个Task。
这个时候再去看MoveNext里头的代码就会发现,try块里的代码再次变清净了。。。而这一次WriteLine的泛型参数就变成了object。
关键中的关键在于,这一个版本中不存在TaskAwaiter,也不存在TaskAwaiter.GetResult(详情参见上一段代码第30行)。
GetResult的实现如下:

 // System.Runtime.CompilerServices.TaskAwaiter<TResult>
[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public TResult GetResult()
{
TaskAwaiter.ValidateEnd(this.m_task);
return this.m_task.ResultOnSuccess;
}

这就是Task<TResult>转变为TResult的地方了。

六、使用示例

扯了这么多,扯得这么乱,我自己都晕乎了。。。
到底该怎么用嘛,看示例吧:

 void PagePaint()
{
Console.WriteLine("Paint Start");
Paint();
Console.WriteLine("Paint End");
} void Paint()
{
Rendering("Header");
Rendering(RequestBody());
Rendering("Footer");
} string RequestBody()
{
Thread.Sleep();
return "Body";
}

假设有这么个页面布局的方法,依次对头部、主体和底部进行渲染,头部和底部是固定的内容,而主体需要额外请求。
这里用Sleep模拟网络延时,Rendering方法其实也就是对Console.WriteLine的简单封装而已。。。
PagePaint运行过后,结果是这样的:

Paint Start
Header
Body
Footer
Paint End

挺正常的结果,但是Header渲染完以后页面就阻塞了,这个时候用户没法对Header进行操作。
于是就进行这样的修正:

 async void Paint()
{
Rendering("Header");
Rendering(await RequestBody());
Rendering("Footer");
} async Task<string> RequestBody()
{
return await Task.Run(() =>
{
Thread.Sleep();
return "Body";
});
}

运行结果变成了这样:

Paint Start
Header
Paint End
Body
Footer

这样就能在Header出现之后不阻塞主线程了。

不过呢,Footer一直都得等到Body渲染完成后才能被渲染,这个逻辑现在看来还没问题,因为底部要相对于主体进行布局。
然而我这时候又想给页面加一个广告,而且是fixed定位的那种,管啥头部主体想盖住就盖住,你们在哪它不管。
比如这样写:

 async void Paint()
{
Rendering(await RequestAds());
Rendering("Header");
Rendering(await RequestBody());
Rendering("Footer");
}

出现了很严重的问题,头部都得等广告加载好了才能渲染,这样显然是不对的。
所以应该改成这样:

 async void Paint()
{
PaintAds();
Rendering("Header");
Rendering(await RequestBody());
Rendering("Footer");
} async void PaintAds()
{
string ads = await Task.Run(() =>
{
Thread.Sleep();
return "Ads";
});
Rendering(ads);
}

这样的运行结果就算令人满意了:

Paint Start
Header
Paint End
Ads
Body
Footer

最后想说的是,看IL比看bytecode实在麻烦太多了,CSC对代码动的手脚比JavaC多太多了。。。然而非常值得高兴的是,MS所做的这一切,都是为了让我们写的代码更简洁易懂,我们需要做的,就是把这些语法糖好好地利用起来。

[C#]剖析异步编程语法糖: async和await的更多相关文章

  1. 【转】剖析异步编程语法糖: async和await

    一.难以被接受的async 自从C#5.0,语法糖大家庭又加入了两位新成员: async和await. 然而从我知道这两个家伙之后的很长一段时间,我甚至都没搞明白应该怎么使用它们,这种全新的异步编程模 ...

  2. C#基础系列——异步编程初探:async和await

    前言:前面有篇从应用层面上面介绍了下多线程的几种用法,有博友就说到了async, await等新语法.确实,没有异步的多线程是单调的.乏味的,async和await是出现在C#5.0之后,它的出现给了 ...

  3. 多线程编程学习笔记——async和await(二)

    接上文 多线程编程学习笔记——async和await(一) 三.   对连续的异步任务使用await操作符 本示例学习如何阅读有多个await方法方法时,程序的实际流程是怎么样的,理解await的异步 ...

  4. 多线程编程学习笔记——async和await(三)

    接上文 多线程编程学习笔记——async和await(一) 接上文 多线程编程学习笔记——async和await(二) 五.   处理异步操作中的异常 本示例学习如何在异步函数中处理异常,学习如何对多 ...

  5. 【.NET异步编程系列1】:await&async语法糖让异步编程如鱼得水

    前导 Asynchronous programming Model(APM)异步编程模型以BeginMethod(...) 和 EndMethod(...)结对出现. IAsyncResult Beg ...

  6. 异步编程新方式async/await

    一.前言 实际上对async/await并不是很陌生,早在阮大大的ES6教程里面就接触到了,但是一直处于理解并不熟练使用的状态,于是决定重新学习并且总结一下,写了这篇博文.如果文中有错误的地方还请各位 ...

  7. 走进异步编程的世界--async/await项目使用实战

    起因:今天要做一个定时器任务:五分钟查询一次数据库发现超时未支付的订单数据将其状态改为已经关闭(数据量大约100条的情况) 开始未使用异步: public void SelfCloseGpPayOrd ...

  8. 编程概念--使用async和await的异步编程

    Asynchronous Programming with Async and Await You can avoid performance bottlenecks and enhance the ...

  9. 异步编程系列第05章 Await究竟做了什么?

    p { display: block; margin: 3px 0 0 0; } --> 写在前面 在学异步,有位园友推荐了<async in C#5.0>,没找到中文版,恰巧也想提 ...

随机推荐

  1. Git简单操作命令

    Git 1.创建远程分支(git项目已在) git checkout -b cgy git add . git commit -m “add new branch” git push origin c ...

  2. 模板 Template

    package ${enclosing_package}; import java.io.IOException;import javax.servlet.ServletException;impor ...

  3. 跟我学Spring Boot(三)Spring Boot 的web开发

    1.Web开发中至关重要的一部分,Web开发的核心内容主要包括内嵌Servlet容器和SpringMVC spring boot  提供了spring-boot-starter-web 为web开发提 ...

  4. java 开发微信中回调验证一直提示 解密失败处理(Java)

    微信公众号平台接入JDK6和JDK7及JDK8加解密失败处理(Java) 根据自己jdk版本编译,如jdk7或者jdk6 ,此时部署后提示报错:java.security.InvalidKeyExce ...

  5. Linux CPU Hotplug CPU热插拔

    http://blog.chinaunix.net/uid-15007890-id-106930.html   CPU hotplug Support in Linux(tm) Kernel Linu ...

  6. PHP递归函数

    递归函数(Recursive Function)是指直接或间接调用函数本身的函数 在每次调用自己时,必须是(在某种意义上)更接近 于解 必须有一个终止处理或计算的准则 function recursi ...

  7. Java语法基础课 原码 反码 补码

    原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 反码的表示方法是:正数的反码是其本身:负数的反码是在其原码的基础上, 符号位不变,其余各个位取反. 补码的表示方法是在反码的基础 ...

  8. python里的字典和集合

    一.字典 1.字典的定义 字典是不可变的,是用hash值来存储的.字典的key必须是不可变的(可哈希) dict = {key1:value1 , key2:value2} 2.字典的增删改查 增 直 ...

  9. C++ MFC棋牌类小游戏day1

    好用没用过C++做一个完整一点的东西了,今天开始希望靠我这点微薄的技术来完成这个小游戏. 我现在的水平应该算是菜鸟中的战斗鸡了,所以又很多东西在设计和技术方面肯定会有很大的缺陷,我做这个小游戏的目的单 ...

  10. IntelliJ IDEA 2017版 Spring5 的RunnableFactoryBean配置

    1.新建RunnableFactoryBean package com.spring4.pojo; import org.springframework.beans.factory.FactoryBe ...