什么是异步?

你早上起来做饭,把食材放入之后,设定好时间,如果你只是等待,没有离开做别的事,就是同步;但你不会一直等着,多半会玩手机,或者打扫之类的,等到时间到了,或者打扫完了,才去吃饭,那就是异步。

什么是异步编程?

要看异步编程,先看同步编程。什么是同步。同步,synchronous.就是按代码编写的顺序,从上往下顺序执行。

每个Task依次执行,T1执行完,T2才能执行;T3有错误了,T4就不能执行。

一般地,程序正常执行,但是遇到耗时的任务,怎么办?比如大文件上传/下载、调用第三方服务、哈希运算等。执行这些任务时,会使当前线程什么也做不了,持续长时间消耗在这个任务上,或者只是等待第三方调用返回,无法及时处理其他任务。并不能及时处理并发任务。

也就是说,后面的任务依赖于当前任务,当前任务无法完成,就不能继续往下执行。这就阻塞了当前线程。
这该怎么处理?现代处理器的多核架构,我们可以简单粗暴地就直接开一个线程,处理耗时的任务。

如上图,耗时的任务可以放到另一个线程,保证主线程不阻塞。但这也有问题:

  1. 多线程编程很麻烦,如果不同线程间任务有依赖,线程间通信和调度麻烦
  2. 一个线程遇到耗时任务就只能等待,那么计算资源就浪费了,并没有最大化利用硬件资源。

生活中,我们没有连续的一小时来玩游戏,但是你排队吃饭的时候玩10分钟,通勤的时候玩10分钟,睡前玩30分钟,60分钟的分钟都是拆成了更小的任务,一点一点完成。这就实现了异步。

异步是把任务拆分之后交替执行,而不是一个执行完再执行下一个。

显然,异步的处理,需要更多的代码来处理控制逻辑,怎样拆分任务,任务之间的依赖如何处理,等等。如果都是单线程,为什么要选异步呢?操作起来还更复杂。

再来看玩游戏的例子,为什么不等玩游戏结束了再去坐公交?因为坐公交更紧急,而游戏可以之后再玩。UI编程中,总是优先处理点击、输入这些交互事件,而不是等待文件下载,阻塞在那里。

那么异步如何实现?

现代编程有多种异步的实现方式,比如callback、Promise/Future、async/await.先来看一看callback。

什么是callback?

callback,回调。在计算机编程中,一个回调是对某一块可执行代码的引用,被引用的代码作为参数传递给另一块代码。即,call then back.

这个调用在同步回调中就立即执行,在异步回调中就在之后的某个时间执行。也称为阻塞和非阻塞。

C语言中的回调

回调函数还在函数调用的context中执行。通过函数指针来实现同步回调,将一个函数的地址传给另一个函数。

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
#include<stdio.h>

typedef void (*Handler)();

typedef struct {
int height;
Handler onClick;
} Button;

// 注册回调
void register_handler(Button* button, Handler handler) {
button->onClick = handler;
button->onClick();
}

void callbackA() {
printf("button was clicked!\n");
}

void callbackB() {
printf("Don't click any more!\n");
}

int main() {
Button loginButton;
loginButton.height = 10;

register_handler(&loginButton, callbackA);
register_handler(&loginButton, callbackB);

return 0;
}

有没有很像OOP中的控制反转,这就是C语言的IoC实现

但这也是同步执行的啊,回调如何实现异步执行呢?

最简单的就是定时器回调,就好比,遇到重要的事情,暂时玩不了游戏,我们会设置一个提醒,到晚上10点,玩游戏。

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
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <time.h>
#include <unistd.h>

// 定义回调函数类型
typedef void (*callback)(int);

// 定义定时器结构体
typedef struct {
time_t expire;
callback cb;
int args;
} timer;

// 创建定时器
timer create_timer(int delay, callback cb, int data) {
timer t;
t.expire = time(NULL) + delay;
t.cb = cb;
t.args = data;
return t;
}

// 检查定时器是否到期,如果到期则调用回调函数
void check_timer(timer *t) {
if (time(NULL) >= t->expire) {
t->cb(t->args);
}
}

void timer_callback(int data) {
printf("Timer expired, data: %d\n", data);
}

int main() {
// 创建一个2秒后到期的定时器
timer t = create_timer(2, timer_callback, 123);

// 主事件循环
while (1) {
check_timer(&t);
sleep(4); // 模拟事件循环中的其他操作
printf("execute first\n");
}

return 0;
}
1
2
3
4
5
6
7
execute first
Timer expired, data: 123
execute first
Timer expired, data: 123
execute first
Timer expired, data: 123
...

可以看到,先执行了后面的printf语句,时间到了之后,才执行定时器回调中的printf语句。

JS中的回调

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
const arr = [12, 21, 22];
const callback1 = x => x * 2;
arr.map(callback1);


// 异步回调
// timer callback
const callback2 = () => console.log("zz");
setTimeout(callback2, 3000)

// UI callback
const addButton = document.querySelector("#add")
addButton.addEventListener("click", () => {
console.log("You clicked #addButton")
})

function callback3(msg) {
console.log(`${msg}\n`);
}

// network callback
(function fetch() {
const xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
// Typical action to be performed when the document is ready:
callback3(xhr.responseText);
}
};
xhr.open("GET", "https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json", true);

xhr.send();
})()

由于Chrome浏览器的多进程架构,Browser Process中有网络线程、UI线程、存储线程等。所以,JS中有些异步,是通过浏览器的多线程来实现的。

为什么要用异步编程?

换句话说,异步相比于同步,优点在哪里?

上面提到,同步容易阻塞当前线程,而异步是非阻塞的。也就是说对于经常阻塞的情况,异步是优于同步的。好比等车的时候,在地铁上,一般会掏出手机玩游戏,因为必须等,等车到站。那么,什么情况下,线程必须要等呢?等待执行I/O操作。大量的I/O操作,同步线程的执行是这样的:

CPU 的数据传输速率比磁盘或网络连接快几个数量级。因此,同步执行大量 I/O 的程序将在磁盘或网络上花费大量时间阻塞。这就是阻塞IO。异步把需要等待的任务都放到最后,总的等待时间减少了。

所以,异步在这种情况下效率更高:

  1. 任务量大,但总是有任务可以继续

  2. 任务执行了大量I/O,导致同步程序在阻塞时浪费大量时间

  3. 任务之间大部分相互独立,不需要任务间通信

这些条件几乎完美符合了C/S架构中一个繁忙的网络服务器的特点。每个task表示一个客户端请求。异步模型的应用之一就是web服务器,这也是为什么近年来Node.js越来越流行。

参考资料

Introduction to Asynchronous Programming