异步状态机与单线程异步
异步状态机与单线程异步
intro
C#作为第一个应用await async异步关键字的语言,他的异步实现一定值得我们学习和参考。
这个视频就以C#为例,探究一下异步的实现原理,
最后在看看js的异步实现,比较两者的异同。
Task
说起异步,不得不讲C#的Task类型。Task是指一个可等待任务
Task在System.Threading.Tasks命名空间中,这也可以看出,Task在C#中是和多线程有关系的。
这里要重点关注Task的几个关键属性和方法:
GetAwaiter()
用来获取该任务的等待器。等待器顾名思义,就是用来标识任务的完成情况的。等待器的方法GetResult()
可以阻塞拿到等待器对应的任务的结果.
等待器还有一个重要方法,OnCompleted()
, 该方法可以设置等待器的回调。这其实与js中的Promise.then功能相同。
状态机
C#的异步靠的是状态机。具体是AsyncVoidMethodBuilder
和 IAsyncStateMachine
IAsyncStateMachine.MoveNext()
用来切换状态机的状态。
AsyncVoidMethodBuilder.AwaitOnCompleted(TAwait, TStateMachine)
接受两个参数,一个是任务等待器,一个是状态机实例,当等待器标记完成后,该方法会自动切换状态机的状态(调用MoveNext)
异步实现
现在了解了前置知识,就可以尝试实现await的功能
1
2
3
4
5
6
7
8
9
public static async void DoSomethingAsync()
{
await Task.Delay(1000);
Console.WriteLine("Done 1");
await Task.Delay(1000);
Console.WriteLine("Down 2");
await Task.Delay(1000);
Console.WriteLine("Done 3");
}
实验的方法中有三个await语句,await的语义为“等待完成但不阻塞”,也就是说,该方法在await任务完成前,是不会继续往下执行,而是直接返回调用方,直到await任务完成后,才往下执行。
这里有两个关键点:等待的时候返回调用方,也就是非阻塞;完成后继续向后执行,这就需要用到AwaitOnCompleted
,回调方法。
到这里,聪明的朋友应该想到了,如何把上面的样例方法包装成状态机了。
三个await语句,就对应了三个状态,这样当样例方法执行到第一个await,就把状态标记为1,并设置好等待器回调,然后直接返回。当任务完成,回调触发,再次进入状态机,由于根据标记,执行样例方法的第二部分。以此类推。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class DoSomethingStateMachine : IAsyncStateMachine{
public int state;
public Awaiter awaiter;
public void MoveNext(){
if(state == -1){
var awaiter1 = Task.Delay(1000).GetAwaiter();
if(!awaiter1.IsCompleted){
state = 1;
awaiter = awaiter1;
AsyncVoidMethodBuilder.AwaitOnCompleted(awaiter1, this);
return;
}else{
}
}
if(state == 1){
var awaiter2 = Task.Delay(1000).GetAwaiter();
if(!awaiter2.IsCompleted){
state = 2;
awaiter = awaiter2;
AsyncVoidMethodBuilder.AwaitOnCompleted(awaiter2, this);
return;
}
}
if(state == 2){
var awaiter3 = Task.Delay(1000).GetAwaiter();
if(!awaiter3.IsCompleted){
state = 3;
awaiter = awaiter3;
AsyncVoidMethodBuilder.AwaitOnCompleted(awaiter3, this);
return;
}
}
// ...
}
}
异步常见问题
异步一定需要多线程吗?
在C#中,异步关键字await/async的实现,靠的是Task,而Task的执行是交给线程池的。线程池是由.NET运行时管理的。所以异步在C#中是依赖于多线程的,至少 .net 程序是有一个 主线程,以及 托管线程池。而线程池中的工作线程数量,是可以配置的,但即便配置为1,异步也可以正常工作。
但需要注意的是,C#的await、async是依靠多线程,但异步这个功能,并不只有状态机+线程池一种实现方式。比如后面我们会提到js的await、async
异步可以在单核心计算机工作吗?
毫无疑问是可以的。
对这个问题有疑问的同学,一定是混淆了进程和线程的概念。单核心CPU可以通过时间片划分,来“同时”运行不同的应用程序,而每一个应用就是一个进程。进程拥有自己的上下文环境,进程的切换由操作系统来管理。因此程序员并不需要关心CPU的核心数量。
同理,应用程序可能需要一些主逻辑之外的处理器资源,来执行一些辅助任务,因此操作系统也提供了线程模型。无论是线程还是进程,都是面向程序员的对CPU资源的抽象,并不对应于真正的物理CPU的核心数量
因为C#的异步并不是面向CPU核心的,CPU的计算资源是由OS来管理的,通过时间片划分,即便是单核心处理器,OS也可以提供应用程序多线程功能。
与Javascript的比较
js是单线程的,但是js也有await/async,既然说异步依赖于多线程,那js是怎么实现异步的?
js确实是单线程,正因如此,js的await实现方式与C#完全不同。js使用“事件循环”来处理异步任务。
由于只有一个主线程,因此js引擎将主线程的执行过程分为以下几个块:同步任务(包括js脚本),异步任务(宏任务和微任务)。
首先同步任务会占用主线程,直到同步任务完成,其次,执行异步任务。微任务优先级要高于宏任务,在下一个宏任务开始之前,需要将微任务队列清空。
如果我们用C#来实现类似的事件循环,那就长这样
public static class Javascript
{
public static Queue<Action> MacroQueue = new Queue<Action>();
public static Queue<Action> MicroQueue = new Queue<Action>();
public static Action SyncAction;
public static void SyncMain()
{
SyncAction();
while (MicroQueue.Count + MacroQueue.Count > 0)
{
while (MicroQueue.Count > 0)
{
MicroQueue.Dequeue()();
}
while(MacroQueue.Count > 0)
{
while (MicroQueue.Count > 0)
{
MicroQueue.Dequeue()();
}
MacroQueue.Dequeue()();
}
}
}
}
现在我们回到之前的问题:
异步到底是不是必须依赖于多线程?
现在我们可以回答,不依赖。异步的实现方式可以有多种,C#中是“状态机+线程池“,JS中是”事件循环“。异步这个概念本身,是为了保证主线程的响应性。