Bootstrap

C#异步多线程——浅谈async/await底层原理

async/await是块语法糖,编译器帮助我们做了很多工作,下面我们就简单剖析一下async/await的底层原理。

反编译工具ILSpy安装

我用的是ILSpy反编译生成的dll程序集。还没有ILSpy工具的小伙伴可以直接在VS中安装;点击Extensions=>Manage Extensions,搜索ILSpy,按步骤下载安装即可,重启VS在Tool中打开就可以使用了;有可能我们用的.NET版本低,提示需要安装一个高版本的运行时环境,按照步骤下载安装就行,非常简单。
在这里插入图片描述
使用时注意把C#的版本换的低一些,使用低版本我们才方便看到更多细节;视图设置为显示所有类型和成员。
在这里插入图片描述

入门分析

分析源码本身就是一件需要细心,耐心,又极度枯燥的事,尤其是接下来我们要看的代码是反编译出来的编译器给我们生成的很底层的代码,这不像我们自己写程序还可以加点打印,或者设置个断点去调试下,一旦if语句一多,可能程序该进哪个分支我们都要蒙圈了,代码追的越深越难理解。我们不是“学院派”,先摆正自己的目的,我们要的是比使用更高一个层次,简单了解下背后的原理即可。

简单示例

我是基于.NET6创建了一个控制台项目,不使用顶级语法。项目非常简单,没有什么实际意义,就是为了展示底层原理。

static async Task Main(string[] args)
{
    Console.WriteLine("Project start!");
    await TestAsync();

    Console.WriteLine("TestAsync执行结束");
    await Task.Delay(1000);

    Console.WriteLine("等待1s");
    await Task.Delay(2000);

    Console.WriteLine("Project end!");
}

static Task TestAsync()
{
    return Task.Run(() =>
    {
        Thread.Sleep(1000);
        Console.WriteLine("TestAsync");
    });
}

异步Main

[SpecialName]
[DebuggerStepThrough]
private static void <Main>(string[] args)
{
	Main(args).GetAwaiter().GetResult();
}

[AsyncStateMachine(typeof(<Main>d__0))]
[DebuggerStepThrough]
private static Task Main(string[] args)
{
	<Main>d__0 stateMachine = new <Main>d__0();
	stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
	stateMachine.args = args;
	stateMachine.<>1__state = -1;
	stateMachine.<>t__builder.Start(ref stateMachine);
	return stateMachine.<>t__builder.Task;
}
  • 怎么有两个Main?
    写过异步方法的都知道,如果方法内使用了await,方法声明就必须用async修饰,编译器为了能让我们在Main方法中调用异步方法,也是煞费苦心,直接搞出两个Main,一个是我们熟悉的void Main,另一个是我们项目中的Task Main,编译器在入口Main中调用了一下我们的异步Main。
  • Main方法中怎么跟我们的业务完全不同?
    我们来看看Main方法中干了什么事。
    • 创建了一个类型为<Main>d__0 的状态机 stateMachine。
    • 初始化了一些成员变量:
      • <>t__builder:异步Main方法的核心,负责异步操作,相当于引擎,提供Start方法启动状态机
      • <>1__state:状态机当前状态
    • 调用Sart启动状态机执行我们的异步方法。
  • 所以我们真正的业务就在这个stateMachine状态机中,了解状态机的应该都知道,状态机是一个被多次调用的程序,通过切换状态来决定具体执行哪部分代码。

Start启动状态机

//AsyncTaskMethodBuilder结构体
public void Start<[Nullable(0)] TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
	AsyncMethodBuilderCore.Start(ref stateMachine);
}

public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
	if (stateMachine == null)
	{
		ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
	}
	Thread currentThread = Thread.CurrentThread;
	ExecutionContext executionContext = currentThread._executionContext;
	SynchronizationContext synchronizationContext = currentThread._synchronizationContext;
	try
	{
		stateMachine.MoveNext();
	}
	finally
	{
		if (synchronizationContext != currentThread._synchronizationContext)
		{
			currentThread._synchronizationContext = synchronizationContext;
		}
		ExecutionContext executionContext2 = currentThread._executionContext;
		if (executionContext != executionContext2)
		{
			ExecutionContext.RestoreChangedContextToThread(currentThread, executionContext, executionContext2);
		}
	}
}

这里我们能看懂的就是这句stateMachine.MoveNext();,下面我们重点看一下MoveNext ,这才是真正的状态机处理方法。

MoveNext

private sealed class <Main>d__0 : IAsyncStateMachine
{
	public int <>1__state;
	public AsyncTaskMethodBuilder <>t__builder;
	public string[] args;
	private TaskAwaiter <>u__1;

	private void MoveNext()
	{
		//在异步Main方法中我们初始化<>1__state=-1
		int num = <>1__state;
		try
		{
			TaskAwaiter awaiter3;
			TaskAwaiter awaiter2;
			TaskAwaiter awaiter;
			switch (num)
			{
			default:
				Console.WriteLine("Project start!");
				//TaskAwaiter是个很重要的对象,用来监测TestAsync的运行状态
				//TestAsync只是启动个线程去执行其它任务,这里不会等待,程序继续向下执行
				awaiter3 = TestAsync().GetAwaiter();
				//一般来说异步任务比较耗时,大概率程序会进入该分支				
				if (!awaiter3.IsCompleted)
				{
					//状态机状态从-1变为0				
					num = (<>1__state = 0);
					<>u__1 = awaiter3;
					<Main>d__0 stateMachine = this;
					//这里很重要,用于配置TestAsync完成后的延续					
					<>t__builder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine);
					return;
				}
				goto IL_008a;
			case 0:
				awaiter3 = <>u__1;
				<>u__1 = default(TaskAwaiter);
				num = (<>1__state = -1);
				goto IL_008a;
			case 1:
				awaiter2 = <>u__1;
				<>u__1 = default(TaskAwaiter);
				num = (<>1__state = -1);
				goto IL_00f9;
			case 2:
				{
					awaiter = <>u__1;
					<>u__1 = default(TaskAwaiter);
					num = (<>1__state = -1);
					break;
				}
				IL_00f9:
				awaiter2.GetResult();
				Console.WriteLine("等待1s");
				awaiter = Task.Delay(2000).GetAwaiter();
				if (!awaiter.IsCompleted)
				{
					num = (<>1__state = 2);
					<>u__1 = awaiter;
					<Main>d__0 stateMachine = this;
					<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
					return;
				}
				break;
				IL_008a:
				awaiter3.GetResult();
				Console.WriteLine("TestAsync执行结束");
				awaiter2 = Task.Delay(1000).GetAwaiter();
				if (!awaiter2.IsCompleted)
				{
					num = (<>1__state = 1);
					<>u__1 = awaiter2;
					<Main>d__0 stateMachine = this;
					<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
					return;
				}
				goto IL_00f9;
			}
			awaiter.GetResult();
			Console.WriteLine("Project end!");
		}
		catch (Exception exception)
		{
			<>1__state = -2;
			<>t__builder.SetException(exception);
			return;
		}
		<>1__state = -2;
		<>t__builder.SetResult();
	}

	void IAsyncStateMachine.MoveNext()
	{
		this.MoveNext();
	}
}

接下来我们对MoveNext的调用进行梳理;
MoveNext的第一次调用:

  • 开始状态机状态为-1,程序先进入switch中的default分支,这里我们看到了我们所写的第一句代码Console.WriteLine("Project start!");
  • 接着执行异步方法TestAsync(),获得一个等待器(TaskAwaiter),这个对象也非常重要,里面有个Task类型的变量m_task,保存了异步方法返回的Task对象。
  • 我们在TestAsync里进行了Sleep,比较耗时,不会立即完成所以会进入if (!awaiter3.IsCompleted)分支
    • 切换状态机状态:<>1__state = 0
    • 调用AwaitUnsafeOnCompleted方法,参数传入等待器和状态机对象,用于配置TestAsync完成后的延续,也就是再次调用MoveNext。
    • 最后return,也就是第一次调用MoveNext结束了。

这里我们小结一下,第一次状态机的调用对应我们的代码,执行了前两句

Console.WriteLine("Project start!");
await TestAsync();

MoveNext的第二次调用:

  • 此时状态机状态为0,进入case 0分支,这里又对状态机状态初始化为了<>1__state = -1,然后goto跳转到I L_008a
  • 终于又执行了我们写的下一行代码Console.WriteLine("TestAsync执行结束");
  • 接着执行Task.Delay(1000),获得等待器
  • 很显然执行这个任务要1s钟,不会立即完成,所以会进入if (!awaiter2.IsCompleted)分支
    • 切换状态机状态:<>1__state = 1
    • 调用AwaitUnsafeOnCompleted方法,参数传入等待器和状态机对象,用于配置Task.Delay(1000)完成后的延续,也就是再次调用MoveNext。
    • 最后return,也就是第二次调用MoveNext结束了。

这里我们小结一下,第二次状态机的调用对应我们的代码,执行了:

Console.WriteLine("TestAsync执行结束");
await Task.Delay(1000);

MoveNext的第三次调用:

  • 此时状态机状态为1,进入case 1分支,这里又对状态机状态初始化为了<>1__state = -1,然后goto跳转到 IL_00f9
  • 又执行了我们写的下一行代码Console.WriteLine("等待1s");
  • 接着执行Task.Delay(2000),获得等待器
  • 很显然执行这个任务要2s钟,不会立即完成,所以会进入if (!awaiter.IsCompleted)分支
    • 切换状态机状态:<>1__state = 2
    • 调用AwaitUnsafeOnCompleted方法,参数传入等待器和状态机对象,用于配置Task.Delay(2000)完成后的延续,也就是再次调用MoveNext。
    • 最后return,也就是第三次调用MoveNext结束了。

这里我们小结一下,第三次状态机的调用对应我们的代码,执行了:

Console.WriteLine("等待1s");
await Task.Delay(2000);

MoveNext的第四次调用:

  • 此时状态机状态为2,进入case 2分支,这里又对状态机状态初始化为了<>1__state = -1,然后break跳出switch
  • 执行我们写的最后一句代码Console.WriteLine("Project end!");
  • 最后切换状态机状态:<>1__state = -2,代码执行完了,不需要再配置延续了,MoveNext也不会再被调用了。

这里我们小结一下,第四次状态机的调用对应我们的代码,执行了:

Console.WriteLine("Project end!");

所以上面对MoveNext的四次调用对应到我们的代码执行为:
在这里插入图片描述

总结

  • async方法会被C#编译器编译成一个状态机类,根据await调用进行切分成多个状态,对async方法的调用会被拆分为多次对MoveNext的调用。

async方法不启用多线程

一看到异步自然而然就会和多线程关联起来,那我们就是要写一个不使用多线程的async方法,看看底层又做了什么。

简单示例

static async Task Main(string[] args)
{
    Console.WriteLine("Project start!");
    await TestFakeAsync();

    Console.WriteLine("TestAsync执行结束");
    await Task.Delay(1000);

    Console.WriteLine("等待1s");
    await Task.Delay(2000);

    Console.WriteLine("Project end!");
}

static async Task TestFakeAsync()
{
    Thread.Sleep(1000);
    Console.WriteLine("TestFakeAsync");
}

MoveNext

我们的异步Main方法仍然加了async,方法内也使用了await调用,所以对异步Main的处理和上面入门分析的执行逻辑没什么不同。前面的启动过程就不列举了,我们直接看下MoveNext。

private void MoveNext()
{
	int num = <>1__state;
	try
	{
		TaskAwaiter awaiter3;
		TaskAwaiter awaiter2;
		TaskAwaiter awaiter;
		switch (num)
		{
		default:
			Console.WriteLine("Project start!");
			//TestFakeAsync没有启动多线程,内部是同步执行,比较耗时
			awaiter3 = TestFakeAsync().GetAwaiter();
			//TestFakeAsync返回时方法是执行完成的,所以不会进入分支
			if (!awaiter3.IsCompleted)
			{
				num = (<>1__state = 0);
				<>u__1 = awaiter3;
				<Main>d__0 stateMachine = this;
				<>t__builder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine);
				return;
			}
			goto IL_008a;
		case 0:
			awaiter3 = <>u__1;
			<>u__1 = default(TaskAwaiter);
			num = (<>1__state = -1);
			goto IL_008a;
		case 1:
			awaiter2 = <>u__1;
			<>u__1 = default(TaskAwaiter);
			num = (<>1__state = -1);
			goto IL_00f9;
		case 2:
			{
				awaiter = <>u__1;
				<>u__1 = default(TaskAwaiter);
				num = (<>1__state = -1);
				break;
			}
			IL_00f9:
			awaiter2.GetResult();
			Console.WriteLine("等待1s");
			awaiter = Task.Delay(2000).GetAwaiter();
			if (!awaiter.IsCompleted)
			{
				num = (<>1__state = 2);
				<>u__1 = awaiter;
				<Main>d__0 stateMachine = this;
				<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
				return;
			}
			break;
			IL_008a:
			awaiter3.GetResult();
			Console.WriteLine("TestAsync执行结束");
			awaiter2 = Task.Delay(1000).GetAwaiter();
			if (!awaiter2.IsCompleted)
			{
				num = (<>1__state = 1);
				<>u__1 = awaiter2;
				<Main>d__0 stateMachine = this;
				<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
				return;
			}
			goto IL_00f9;
		}
		awaiter.GetResult();
		Console.WriteLine("Project end!");
	}
	catch (Exception exception)
	{
		<>1__state = -2;
		<>t__builder.SetException(exception);
		return;
	}
	<>1__state = -2;
	<>t__builder.SetResult();
}

眼睛都看疼了,除了调用的异步方法名字改了和上面入门分析的MoveNext完全一样,那我们就来好好看看这个TestFakeAsync。

TestFakeAsync

private static Task TestFakeAsync()
{
	<TestFakeAsync>d__1 stateMachine = new <TestFakeAsync>d__1();
	stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
	stateMachine.<>1__state = -1;
	stateMachine.<>t__builder.Start(ref stateMachine);
	return stateMachine.<>t__builder.Task;
}

private sealed class <TestFakeAsync>d__1 : IAsyncStateMachine
{
	public int <>1__state;
	public AsyncTaskMethodBuilder <>t__builder;

	private void MoveNext()
	{
		int num = <>1__state;
		try
		{
			Thread.Sleep(1000);
			Console.WriteLine("TestFakeAsync");
		}
		catch (Exception exception)
		{
			<>1__state = -2;
			<>t__builder.SetException(exception);
			return;
		}
		<>1__state = -2;
		<>t__builder.SetResult();
	}

	void IAsyncStateMachine.MoveNext()
	{
		this.MoveNext();
	}
}

TestFakeAsync也是有async关键字修饰的,所以编译器也同样把它处理为状态机,同样通过stateMachine.<>t__builder.Start(ref stateMachine);启动状态机,并且第一次调用MoveNext。接下来我们对MoveNext的调用进行梳理;

  • 开始状态机状态为-1,程序开始执行我们的代码Thread.Sleep(1000);,接着同步执行第二句Console.WriteLine("TestFakeAsync");
  • 最后状态切换为<>1__state = -2;

总结

  • 异步方法TestFakeAsync,没有启动多线程,里面没有用到await,代码没有分块处理,所以反编译我们看到也没有调用AwaitUnsafeOnCompleted配置任务延续,虽然被编译为状态机,但MoveNext只调用一次,TestFakeAsync方法内的代码都是同步执行,直到结束。

  • 我们看下TestFakeAsync同步执行导致的连锁反应:

    • 首先,因为同步执行,耗时操作都在TestFakeAsync状态机的MoveNext里,所以执行stateMachine.<>t__builder.Start(ref stateMachine);会比较耗时,不会立即完成;
    • 最后return stateMachine.<>t__builder.Task;返回Task结果比较慢。
    • 异步Main状态机中的MoveNext执行awaiter3 = TestFakeAsync().GetAwaiter();就不会立即拿到等待器
    • 拿到等待器,表示任务已经执行完了所以异步Main中第一次调用MoveNext不会进入if (!awaiter3.IsCompleted)分支;
    • 这异步Main中用了await相当于白用了,代码还是同步执行了。
  • 我们对比看下入门分析例子中的TestAsync的连锁反应:

    • TestAsync里面使用Task.Run启用了子线程,耗时任务在子线程中执行,但Run方法是立即返回的
    • 所以异步Main中调用MoveNext执行awaiter3 = TestAsync().GetAwaiter();是立即拿到等待器的。
    • 因为等待器提前拿到,而耗时任务在子线程中执行还没结束,所以第一次调用MoveNext会进入if (!awaiter3.IsCompleted)分支;
  • 刚开始接触async/await的小伙伴可能有个理解上的误区,认为加上async既然是异步编译器会自动帮我们把程序封装到一个子线程中执行,其实async不等于多线程:异步方法的代码并不会自动在新的线程中执行,除非手动把代码放到新线程中执行。

  • 看了上面的分析和例子,你还会给一个同步方法加上async吗?应该不会了,加上asyc编译器生成了那么多代码,执行起来还是同步的,是不是更影响效率了。

async方法不使用await

What?这么简单还要分析?直接贴出来代码方便和上面对比,剩下的自己分析吧,哈哈。

static async Task Main(string[] args)
{
    Console.WriteLine("Project start!");
    TestNoAwaitAsync();

    Console.WriteLine("TestAsync执行结束");
    Task.Delay(1000);

    Console.WriteLine("等待1s");
    Task.Delay(2000);

    Console.WriteLine("Project end!");
}

static Task TestNoAwaitAsync()
{
    return Task.Run(() =>
    {
        Thread.Sleep(1000);
        Console.WriteLine("TestAsync");
    });
}

直接上反编译:

private void MoveNext()
{
	int num = <>1__state;
	try
	{
		Console.WriteLine("Project start!");
		TestAsync();
		Console.WriteLine("TestAsync执行结束");
		Task.Delay(1000);
		Console.WriteLine("等待1s");
		Task.Delay(2000);
		Console.WriteLine("Project end!");
	}
	catch (Exception exception)
	{
		<>1__state = -2;
		<>t__builder.SetException(exception);
		return;
	}
	<>1__state = -2;
	<>t__builder.SetResult();
}

是不是很简单,没有await就不会调用AwaitUnsafeOnCompleted配置任务延续了,也没必要获取等待器了,这个状态机也就执行这一次。

;