同步和异步描述调用者会不会主动等待函数的返回值,举个例子:
public void method() {
int result = otherMethod();
}
像上面这种形式就叫同步,result 会一直等待 otherMethod() 方法执行完毕并拿到返回值
public void method() {
new Thread(() -> {
int result = otherMethod();
});
}
像上面这种形式就叫异步,主线程不会等待 otherMethod() 方法执行完,一般情况下异步处理会配合回调通知调用方执行结果,回调的方式有很多:回调方法、发送 MQ、调 RPC 接口都可以实现,当然没有回调处理也没问题
总得来说,同步和异步关注调用方是否等待函数的返回值,只要不等待就是异步
阻塞描述的是方法本身在等待某一事件的结果时,是将线程阻塞还是立即返回一个未就绪的信息
某个方法阻塞一定包含某种阻塞操作,I/O 读、写就是最常见的阻塞操作。下面写一段伪代码:
public int read() {
while (磁盘未就绪) {
线程阻塞,直到磁盘就绪
}
从磁盘中读取数据
return 读取到的字节数;
}
如上方法就是一个阻塞方法,线程在判断磁盘未就绪时会主动阻塞,直到磁盘就绪
public int read() {
while (磁盘未就绪) {
return -1;
}
从磁盘中读取数据
return 读取到的字节数;
}
如上方法就是一个非阻塞方法,线程在判断磁盘未就绪时会直接返回,不会阻塞,不会让出 CPU 资源
也就是说,阻塞关注的是方法本身,包含阻塞调用就是阻塞方法,否则就不是阻塞方法
也就是说同步、异步 和 阻塞、非阻塞没有关系,两者是不同维度的概念。下面依次介绍每种情况:
- 同步阻塞:线程调用阻塞方法,由于某种原因方法阻塞,线程阻塞,直到阻塞状态消除,方法执行完毕后拿到返回值
- 异步阻塞:主线程异步调用阻塞方法,调用方法后主线程去干别的事情,不等待返回结果,异步开启的线程和上面同步阻塞处理相同
- 同步非阻塞:线程调用阻塞方法,由于某种原因方法没法执行,直接返回失败,此时线程可以考虑循环调用或者去干别的事情
- 异步非阻塞:主线程异步调用阻塞方法,调用方法后主线程去干别的事情,不等待返回结果,异步开启的线程和上面同步非阻塞处理相同
下面通过买书的场景介绍每种情况,方便理解:
- 同步阻塞:我去前台买书,老板让我在前台等会他去找,我一直在前台等,直到老板拿来书
- 异步阻塞:我找了个小弟派他去前台找老板买书,老板让小弟在前台等会他去找,小弟一直在前台等,我去干别的事情
- 同步非阻塞:我去前台买书,老板去找书并让我先去干别的事
- 异步非阻塞:我找了个小弟派他去前台找老板买书,老板去找书并让小弟先去干别的事
老板让我去干别的事,我可以选择继续在前台等,也可以选择去干别的事,这块根据代码判断是否循环调用
至于老板找到书之后会不会通知属于回调,异步可以不包含回调机制
很多地方习惯把阻塞和同步混为一谈,认为只要阻塞就是同步,只要同步就是阻塞调用,实际上这是不对的
最后从操作系统层面理解阻塞到底是什么,上面提到阻塞常用在和 I/O 相关的场景,牵扯到 I/O 就需要调用底层系统调用
举个例子:当我们调用 java 的 readLine() 方法时程序会阻塞等待用户输入回车,实际它调用的就是 linux 操作系统中的 read() 方法
linux 将所有系统调用注册在系统调用表中,方法以 sys_ 开头,也就是说,read() 方法实际调用 sys_read() 方法,sys_read() 方法再经过一系列文件操作之后,可以看到如下代码:
if (EMPTY (tty->secondary)) {
sleep_if_empty (&tty->secondary);
}
static void sleep_if_empty (struct tty_queue *queue) {
cli ();
while (EMPTY (*queue))
interruptible_sleep_on (&queue->proc_list);
sti ();
}
void interruptible_sleep_on (struct task_struct **p) {
current->state = TASK_INTERRUPTIBLE;
schedule ();
}
简单总结就是说:java 程序调用 readLine() 方法最终调用 linux 系统调用 sys_read() 方法,在该方法最后只要用户不输入回车就调用 interruptible_sleep_on() 方法将进程的状态修改为 TASK_INTERRUPTIBLE,即可中断的状态,并调用 schedule() 方法
根据 java 线程模式来看,java 线程采用 1 对 1 操作系统进程的方式实现,一个 java 线程实际就对应 linux 一个轻量级进程,所以下文在操作系统层面均以进程来介绍,大家知道就好
schedule() 方法会强制执行一次进程调度,进程调度主要做三件事:
- 拿到剩余时间片(counter)最大并且处于 runnable 状态的进程号 next
- 如果所有 runnable 状态进程的时间片都为 0,重新设置所有进程(各种状态)的 counter 值,继续执行步骤 1
- 拿到进程号 next,调用 switch_to(next) 方法,切换到对应进程执行
也就是说,进程调度时只会跳转到处于 runnable 状态的进程,而阻塞的进程处于 TASK_INTERRUPTIBLE 状态,一定不会被调度,所谓阻塞就是通过这种方式告诉进程调度,该进程放弃 CPU 的执行权,进程被阻塞
在底层操作系统层面,进程由于处于 TASK_INTERRUPTIBLE 状态放弃 CPU 调度权,而对于上层应用来说线程一直停在那里,就好像挂起了一样,这也就是阻塞的本质
有挂起就有恢复,对于 readLine() 方法,当我们按下回车键后,触发键盘中断,执行一系列中断方法后最终来到以下代码:
wake_up(&tty->secondary.proc_list);
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state = TASK_RUNNABLE;
*p = NULL;
}
}
很明显,将进程的状态又重新修改回 TASK_RUNNABLE,此时该进程又可以被 CPU 调度了,也就是说进程恢复了
最后总结一下,所谓线程阻塞和恢复到操作系统层面实际都是通过修改进程状态,只有 RUNNABLE 状态的进程才能被操作系统调度,否则就像不动了一样,永远无法获取 CPU 资源,一直阻塞。一句话:挂起、恢复都是通过修改进程状态实现,其它交给进程调度
参考:微信公众号《低并发编程》