同步异步描述的是被调用方。阻塞非阻塞描述的是调用方。二者没有必然联系。
- 阻塞是调用方A发出命令后,必须等待B返回结果。非阻塞是调用方A发出命令后,A不需要等待B,可以做自己的事情。
- 同步是B收到A的指令之后会立即执行,A可以得到结果。异步是B收到A的指令之后不会立即执行要做的事情,A的本次调用不会得到结果,但是B执行完要做的事情之后会通知A。
我们常常混淆的同步异步、阻塞非阻塞其实是放在特定场景下的,不能一概而论,IO也分为磁盘IO和网络IO。这里所讲的IO一般指网络IO。
- 阻塞IO和非阻塞IO:指的是socket编程中发起read函数系统调用读取数据后是否阻塞住
- 如果一直等待到有数据才返回,这个read就是阻塞的,也是同步的
- 如果没有数据就返回-1而不是等待,这个read就是非阻塞的,也是同步的
- 同步IO和异步IO:指的操作系统内核是否自动将数据从内核空间拷贝到用户空间
- 如果需要read函数自己将数据拷贝到用户空间就是同步IO
- 如果内核自动将数据拷贝到用户空间,并且通知用户,就是异步IO(一般在Linux上用的少,windows有完整实现)
- 同步执行和异步执行:指的是是否按照代码编写的顺序执行
- 如果按照代码位置顺序执行,就是同步执行
- 如果像js里面的setTimeout,设置了callback之后,执行时候直接执行后面的。到达设定时间才开始执行前面设定的callback函数。就是异步执行。
- 同步调用和异步调用:指的是是否通过多线程调用函数
- 如果只在单线程内顺序执行方法就是同步调用
- 如果新建线程执行方法就是异步调用
- 同步请求和异步请求:指的是前端发起的是整体页面请求还是局部请求
- 如果是整个网页的请求那就是同步请求
- 如果是ajax这种局部请求,那就是异步请求,一般会定义回调函数用于接收响应后的处理。
网络IO演进分析
同步阻塞网络IO
最开始学习基础Linux的Socket编程时候,流程如下:
// 伪代码
// 创建 Socket 的时候,可以指定网络层使用的是 IPv4 还是 IPv6,传输层使用的是 TCP 还是 UDP。
listenSocketFd = socket();
// 给这个 Socket 绑定一个 IP 地址和端口
bind(listenSocketFd);
// 监听
listen(listenSocketFd);
while(1) {// 阻塞,等待TCP 全连接队列不为空,内核态拿出对应socket返回用户态connSokcetFd = accept(listenSocketFd);fds.add(connSokcetFd);for (fd : fds) {// 阻塞,等待数据就绪,拷贝到用户空间的 bufint n = read(fd, buf); // 使用数据做事情doThing(buf);}
}
客户端和服务端具体的交互流程如下,其中服务端的accept 函数和read 函数都会阻塞,前者等待三次握手结束才会向下执行,后者等待数据就绪才会向下执行(参考小林coding):
这里主要关注read函数,看下图可以知道会一直阻塞直到数据从网卡拷贝到内核缓冲区完成。read才开始执行将数据从内核空间拷贝到用户空间(参考低并发编程):
整个同步阻塞IO的流程如下:
上面同步阻塞IO会导致客户端一直阻塞,新客户端连接也不能处理。一种解决办法就是新建一个线程来处理客户端的请求:
while(1) {// 阻塞,等待TCP 全连接队列不为空,内核态拿出对应socket返回用户态connSokcetFd = accept(listenSocketFd);// 新建线程处理客户端请求pthread_create(doWork);
}
void doWork() {// 阻塞,等待数据就绪,拷贝到用户空间的 bufint n = read(connSokcetFd, buf); // 使用数据做事情doThing(buf); // 关闭连接,循环等待下一个连接close(connSokcetFd);
}
扩展知识:进程和socket在内核中的关系:
同步非阻塞网络IO
使用多线程可以避免一个人阻塞其他人,但是效率还是比较低。在发起系统调用read的时候,依旧是阻塞的。而且多线程在用户多时会导致大量线程创建,不是明智之举。所以需要在操作系统层面提供支持,也就是提供非阻塞的 read 函数:
实现的时候就将客户端创建的连接都放到一个集合里面,然后对集合里面的连接不断执行read直到读取到数据:
// 伪代码
while(1) {// 阻塞,等待TCP 全连接队列不为空,内核态拿出对应socket返回用户态connSokcetFd = accept(listenSocketFd);fds.add(connSokcetFd);for (fd : fds) {// 阻塞,等待数据就绪,拷贝到用户空间的 bufint n = read(fd, buf); if (n != -1) {// 使用数据做事情doThing(buf);}}
}
整个同步非阻塞IO的流程如下:
总结
同步阻塞和非阻塞都是按照写的代码的顺序执行的,只是后者立即返回错误结果,需要代码层面不断轮询直到拿到结果。
但是不断轮询当前所有的连接是否有数据就绪也不是好的办法,因为每一次轮询都是一次系统调用,开销还是比较大的,所以操作系统实现了 IO多路复用 这一利器避免轮询。我是deltaqin,下篇介绍IO多路复用!