async和await详解(C#)
async、await网上讲解的资料已经有很多了,看似没有什么好讲的,但是我发现很多文章还是从理论上讲解了相关的语法糖,懂得都懂,不懂的还是没有懂。
其实async和await在编译器层面只是语法糖,在IL层面还是会显出原型的;所以,在IL层面深入认识它们还是很有必要的。
从IL层面认识
例如如下代码,使用webclient异步下载“https://editor.csdn.net/md?not_checkout=1&spm=1011.2124.3001.6183&articleId=134696726”的HTML。
public class WebClientClass
{
public static void Main(string[] args)
{
var html = GetResult();
Console.WriteLine("稍等......正在下载");
var content = html.Result;
Console.WriteLine(content);
}
static async Task<string> GetResult()
{
var client = new WebClient();
var content = await client.DownloadStringTaskAsync(new Uri("https://editor.csdn.net/md?not_checkout=1&spm=1011.2124.3001.6183&articleId=134696726"));
return content;
}
}
输出结果
上面的代码非常简单,可以看出异步操作并没有阻塞主线程输出。
挖掘async和await的IL代码
从上图可以看出,有一个GetResult
方法,一个Main
方法,还有一个新增加的<GetResult>d__1
类;如果你看不懂,没关系,接下来我们逐一进行讲解。
<GetResult>d__1
因为多了一个这样的类,就勾起了我的好奇心,小手已经按捺不住要去挥动鼠标进行探索了,所以先看看它的IL是长什么样的。
.class nested private auto ansi sealed beforefieldinit '<GetResult>d__1'
extends [System.Runtime]System.Object
implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
{ .method private final hidebysig newslot virtual
instance void MoveNext () cil managed
{
}
.method private final hidebysig newslot virtual
instance void SetStateMachine (
class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine
) cil managed
{
}
}
从上面的IL代码可以看出,是自动生成的<GetResult>d__1
类,实现了接口IAsyncStateMachine
,这个接口的定义如下:
public interface IAsyncStateMachine
{
void MoveNext ();
void SetStateMachine (IAsyncStateMachine stateMachine);
}
当你看到这个接口的MoveNext
方法是不是似曾相识,但是一时间你又想不起来?没错,就是你平时用foreach
集合时,就会使用到这个方法。在foreach
集合中被称之为枚举类,但在这里被改造了,称为状态机。
GetResult
继续看IL中的代码
.method private hidebysig static
class [System.Runtime]System.Threading.Tasks.Task`1<string> GetResult () cil managed
{
IL_0000: newobj instance void ConsoleApp3.Program/'<GetResult>d__1'::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::Create()
IL_000c: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> ConsoleApp3.Program/'<GetResult>d__1'::'<>t__builder'
IL_0011: ldloc.0
IL_0012: ldc.i4.m1
IL_0013: stfld int32 ConsoleApp3.Program/'<GetResult>d__1'::'<>1__state'
IL_0018: ldloc.0
IL_0019: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> ConsoleApp3.Program/'<GetResult>d__1'::'<>t__builder'
IL_001e: ldloca.s 0
IL_0020: call instance void valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::Start<class ConsoleApp3.Program/'<GetResult>d__1'>(!!0&)
IL_0025: ldloc.0
IL_0026: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> ConsoleApp3.Program/'<GetResult>d__1'::'<>t__builder'
IL_002b: call instance class [System.Runtime]System.Threading.Tasks.Task`1<!0> valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::get_Task()
IL_0030: ret
} // end of method Program::GetResult
如果你稍微懂一点IL代码,你就应该知道在IL_0000
处的newobj
,这个方法是做了new <GetResult>d__1
;在IL_002b
处,返回一个get_Task
方法。这时你应该就会明白,为什么主线程不会被阻塞,因为返回的是Task<string>
,最后http
的结果会在Task<string>
中。
将IL代码还原成C#
通过前面一部分的讲解,你应该对async和await在IL层面有一个框架性的认识,这里我将IL代码全部还原成C#代码,看看又有哪些新的发现。
static void Main(string[] args)
{
var html = GetResult();
Console.WriteLine("稍等... 正在下载");
var content = html.Result;
Console.WriteLine(content);
}
static Task<string> GetResult()
{
GetResult stateMachine = new GetResult();
stateMachine.builder = AsyncTaskMethodBuilder<string>.Create();
stateMachine.state = -1;
stateMachine.builder.Start(ref stateMachine);
return stateMachine.builder.Task;
}
public class GetResult : IAsyncStateMachine
{
public int state;
public AsyncTaskMethodBuilder<string> builder;
private WebClient client;
private string content;
private string s3;
private TaskAwaiter<string> awaiter;
public async void MoveNext()
{
var result = string.Empty;
TaskAwaiter<string> localAwaiter;
GetResult stateMachine;
int num = state;
try
{
if(num == 0)
{
localAwaiter = awaiter;
awaiter = default(TaskAwaiter<string>);
num = state = -1;
}
else
{
client = new WebClient();
localAwaiter = client.DownloadStringTaskAsync(new Uri("https://editor.csdn.net/md?not_checkout=1&spm=1011.2124.3001.6183&articleId=134696726"))
.GetAwaiter();
if (!localAwaiter.IsCompleted)
{
num = state = 0;
awaiter = localAwaiter;
stateMachine = this;
builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
}
s3 = localAwaiter.GetResult();
content = s3;
s3 = null;
result = content;
}
catch (Exception ex)
{
state = -2;
client = null;
content = null;
builder.SetException(ex);
}
state = -2;
client = null;
content = null;
builder.SetResult(result);
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
throw new NotImplementedException();
}
}
运行结果和之前一样。
为了方便理解,我将上面的内容整理为一张图。
它基本流程就是:
stateMachine.builder.Start(ref stateMachine) ->
GetResult.MoveNext ->
client.DownloadStringTaskAsync -> localAwaiter.IsCompleted = false -> builder.AwaitUnsafeOnCompleted(ref localAwaiter, ref stateMachine)
-> GetResult.MoveNext -> localAwaiter.GetResult()
-> builder.SetResult(result)
剖析 AsyncTaskMethodBuilder
如果你有仔细的观察,你会发现async和await的异步运作都是由AsyncTaskMethodBuilder
承载的。当异步任务启动时,对结果的封送,与底层IO接触,Task
对应着AsyncTaskMethodBuilder
,Task<string>
对应着AsyncTaskMethodBuilder<string>
。这也是为什么编译器在async
处一直提示你返回Task
和Task<string>
,如果没有Task
和Task<string>
的返回值,就找不到对应的AsyncTaskMethodBuilder
。
然后再了解下AwaitUnsafeOnComplete方法,这个方法也是非常重要
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine> (ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine
{
AwaitUnsafeOnCompleted (ref awaiter, ref stateMachine, ref m_task);
}
程序一旦调用这个方法,在底层IO处理任务完毕后,二次回调此GetResult.MoveNext
方法,表示异常完成任务。
TaskAwaiter
包装Task
结果,封送到builder.SetResult
中。
当你调试上述从IL转换为C#代码时,MoveNext
方法会执行两次。
第一次MoveNext
的触发由stateMachine.builder.Start(ref stateMachine)
开始,如图所示。
第二次的MoveNext回调触发由builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)开始,一旦网络驱动程序处理完毕后,就由线程池IO线程发起到最后触发MoveNext,最后就是执行到awaiter中获取task的result处结束,如图所示。