彻底搞清楚「同步&异步 阻塞&非阻塞」

在谈论到网络I/O时,经常会碰到几个非常容易混淆的概念:同步、异步、阻塞、非阻塞。我们来通过这篇文章彻底搞清楚这些概念!

1. 同步&异步

同步和异步关注的是消息通信机制synchronous communication / asynchronous communication),描述的是调用者,要不要主动等待调用结果,当前线程是否需要等待方法调用执行完毕

  • 同步:就是在发出一个“调用”时,在没有得到结果之前,该“调用”就不返回。换句话说,就是调用者主动等待调用结果

这就是同步:

1
2
3
4
public static void main() {
int result = doSomeThing();
doAnotherThing();
}

主线程调用doSomeThing()方法,会一直等待方法执行完毕并返回结果后再继续执行其他任务。

  • 异步:当一个异步过程调用发出之后,调用者不会立刻得到结果,而是在调用执行完成后被调用者通过回调或者消息机制通知调用者。

这就是异步:

1
2
3
4
5
6
public static void main() {
new Thread(() -> {
int result = doSomeThing(); // doSomeThing()通过回调方法将结果通知到调用者
});
doAnotherThing();
}

主线程创建一个新的线程来执行doSomeThing()方法,方法调用后立即返回(无需等待方法执行完毕),主线程继续执行其他任务,等doSomeThing()方法执行完毕后通过回调机制将结果通知给主线程。

关于同步和异步,举一个通俗的例子:

考虑一个情境,你打电话问书店老板有没有《分布式系统》这本书。

如果是同步机制,书店老板会说:“你稍等,我去查一下”,然后书店老板就查啊查,你也一直在等待书店老板告诉你结果。

而如果是异步机制,书店老板会说:“我去查一下,查好了我再给你回电话”,于是就挂断电话,等查好了书店老板主动打电话告诉你结果(老板这种主动电话告诉结果的方式就是“回调”)。

2. 阻塞&非阻塞

阻塞和非阻塞描述的是函数本身,在等待某一事件的结果时,是将线程挂起,还是立即返回一个未就绪的信息。换句话说,当接口数据未就绪时,线程是否会被挂起。

阻塞和非阻塞一般都是描述IO操作,比如一个读取磁盘数据的函数。

阻塞:

1
2
3
4
5
6
7
8
public int read(byte[] buffer) {
while (磁盘未就绪) {
// 将当前线程挂起,并让出CPU;
}
// 此时磁盘已就绪
doRead() // 真正读取数据到buffer中
return len; // 返回读到的字节数
}

当磁盘未就绪时,会将当前线程挂起,并让出CPU,直到磁盘准备就绪,进行数据读取操作并返回。

非阻塞:

1
2
3
4
5
6
7
8
9
public int read(byte[] buffer) {
if (磁盘未就绪) {
// 立即返回
return -1;
}
// 此时磁盘已就绪
doRead() //真正读取数据到buffer中
return len; // 返回读到的字节数
}

当磁盘未就绪时,直接返回当前磁盘未就绪状态。

至于这个函数调用者采用同步还是异步的方式调用,都不影响这个函数本身是阻塞还是非阻塞的性质。

阻塞IO:应用进程请求数据,内核进程会一直阻塞直到数据准备完成后,并完成数据复制工作,最后返回结果给应用进程。

非阻塞IO:应用进程请求数据,当内核进程发现数据未准备就绪,会立即返回未就绪信息;如果内核进程发现数据已准备就绪,完成数据复制操作并返回成功结果。

关于阻塞和非阻塞,我们仍以前面打电话向书店老板询问是否有《分布式系统》这本书为例。

根据定义,我们知道阻塞和非阻塞是针对“打电话询问书籍”的这个“函数”而言,对于阻塞情况,如果老板暂时不知道有没有这本书的时候,老板会不立即回复你,而是老板自己去查,查啊查,直到找到这本书时再回复你有这本书,然后再挂断电话;而对于非阻塞,如果老板暂时不知道有没有这本书,老板也会立刻回复你“我暂时也不清楚有没有这本书”,随即挂断电话,而不会像前面那种情况一直阻塞在打电话的状态。

关于同步&异步 阻塞&非阻塞的解释,网上有很多说法,甚至很多解释都是错误了,这篇文章是我查看了许多资料并结合自己的理解写出来的,不敢保证完全是正确的,但至少在我看来是非常合理的,如果大家有什么更好的见解欢迎大家留言探讨。