# 关于阻塞非阻塞、异步同步
老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
- 老张把水壶放到火上,立等水开。(同步阻塞),老张觉得自己有点傻
- 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
- 老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大
- 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞) 老张觉得自己聪明了。
- 阻塞是指老张能不能去干其他事
- 同步是指老张通过什么方式知道水开了,自己去看就是同步
# Linux 的 5 种 IO 模型
# 阻塞 I/O(blocking IO)
进程会一直阻塞,直到数据拷贝完成 应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。数据准备好后,从内核拷贝到用户空间,IO函数返回成功指示。阻塞IO模型图如下所示:
# 非阻塞 I/O (nonblocking I/O)
通过进程反复调用IO函数,在数据拷贝过程中,进程是阻塞的。模型图如下所示:
# I/O 复用 (I/O multiplexing)
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。
如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。并且相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。
nio - new IO可以实现IO非阻塞、select,poll,epoll
# 非阻塞I/O
public static void main(String[] args) throws Exception {
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(9090));
ss.configureBlocking(false);
List<SocketChannel> clients = new ArrayList<>();
while (true) {
Thread.sleep(1000);
// 不会在这里阻塞
SocketChannel client = ss.accept();
if (client == null) {
System.out.println("null...");
} else {
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.println("client.port:" + port);
clients.add(client);
}
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
for (SocketChannel c : clients) {
int num = c.read(buffer); // 不会阻塞
if (num > 0) {
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);
String b = new String(aaa);
System.out.println(c.socket().getPort() + ":" + b);
buffer.clear();
}
}
}
}
可以用一个线程可以处理N个accept和client读写;也可以将clients扔给线程池去处理 缺点:
- 无线的循环,无限的系统调用,accept、read
- 1000个client连接,但是只有一个发送了数据,但是也会调用1000次read;每次循环,都会O(n)的系统调用
接车人一直去看每个通道有没有货
# select 和 poll 差别不是很大
应用程序调用内核查询哪些client(文件描述符)有数据了,然后应用程序根据内核返回的文件描述符去对应的client上读(同步)
public class SelectOrPoll {
private ServerSocketChannel server = null;
// selector的实现可以是select、poll、epoll、kqueue
private Selector selector = null;
int port = 9090;
public void initServer() {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
selector = Selector.open();
// 需要监听accept
// selector上可以注册很多很多
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
e.printStackTrace();
}
}
public void start() {
initServer();
try {
while (true) {
// selector给kernel打电话
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
// 处理连接事件
acceptHandler(key);
} else if (key.isReadable()) {
// 处理read事件
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void acceptHandler(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel client = ssc.accept();
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
// 需要监听read
client.register(selector, SelectionKey.OP_READ, buffer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
缺点:询问内核,需要带上所有参数(例如Server,和要问的client);每次调用都要触发内核遍历
# epoll
做法:开辟一个内核空间,用来记录哪里事件已ready,每一个事件ready了就在空间里记录一下,然后应用程序直接查询这个内核空间就可以了
# 信号驱动 I/O (signal driven I/O (SIGIO))
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
# 异步 I/O (asynchronous I/O)
进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。