likes
comments
collection
share

请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型?

作者站长头像
站长
· 阅读数 42

我们在看到异步I/O、非阻塞I/O、I/O多路复用、Reactor模型等等名词时,往往搞不清楚其中的关系,这里做一个简单的梳理。

1. 同步、异步、阻塞、非阻塞

这几个概念在很多博客都有讲到,但是似乎并不准确,我在看了多篇文章之后,将个人的理解记录下:

(1) 消息通信维度

从消息通信的维度讨论时,同步和阻塞以及异步和非阻塞是相同概念,但是需要区分发送方和接收方:

阻塞式发送(同步发送):发送方发送消息后会被阻塞,直到消息被接收方接收到; 非阻塞式发送(异步发送):发送方发送消息后,不需要等待消息被接收方接收到,可以直接进行后续逻辑;

阻塞式接收(同步接收):接收方获取消息时会被阻塞,直到发送方的消息到达; 非阻塞式接收(异步接收):接收方获取消息后,要么接收到一个有效的结果,要么获取到一个null,不会被阻塞。

上述不同类型的发送方式和接收方式可以自由组合,不代表阻塞式发送就必须对应阻塞式接收,非阻塞式发送就必须对应非阻塞接收。

参考这篇文章

(2) I/O维度

从I/O维度考虑,同步/异步与阻塞/非阻塞就有所区别了。

从一次网络I/O的read操作来说,可以把过程分为两部分:

  1. socket缓冲区等待数据准备 阻塞:线程一直阻塞等待数据准备完成; 非阻塞:线程发出请求后,不进行等待,通过轮询/信号量等方式去获取数据是否准备好了。

  2. 将数据从socket缓冲区拷贝到用户空间进程中 同步:准备好数据后,线程需要自己将数据拷贝到用户空间进程中,然后处理数据; 异步:准备好数据后,系统内核负责把数据拷贝到用户空间,然后通知相应线程进行数据处理。

用个烧开水的例子来说明: 我开始烧水之后,一直等待着水开,就是阻塞; 我开始烧水之后,去干别的事情了,过一会回来看一下水有没有烧开,就是非阻塞; 水开了之后,需要过我过来关火,就是同步; 水开了之后,烧水壶自动断电并通知我,就是异步。

因此,根据同步/异步和阻塞/非阻塞的不同,就引申出了不同的I/O模型。

2. I/O模型

(1) 同步阻塞模型

线程发出请求后,阻塞等待数据(阻塞),数据准备好后,由线程将数据拷贝到用户空间(同步)。 请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型? 优点:实现简单,线程阻塞时挂起,不占用CPU资源; 缺点:每个连接需要单独的线程来处理,并发量大时内存和上下文切换时的CPU开销很大。

(2) 同步非阻塞模型

线程发出请求后,不必等待,而是轮询数据是否准备好(非阻塞),数据准备好后,由线程将数据拷贝到用户空间(同步)。 请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型? 优点:不会阻塞线程; 缺点:轮询会不断消耗CPU资源。

(3) 同步多路复用模型

在I/O多路复用模型中,会用到 Select 或 Poll 函数或 Epoll 函数(Linux 2.6 以后的内核开始支持),这些函数也会使线程阻塞,但是和阻塞 I/O 有所不同,由于是内核在进行socket的轮询,因此也认为它是非阻塞模型(因为是轮询等待socket上的读写事件,也有称其为基于事件驱动的非阻塞模型)。

请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型?

以select方法来说,请求发送之后会注册socket到select,select轮询多个socket数据是否准备好(对内核来说非阻塞,对用户线程来说阻塞),数据准备好后,由线程将数据拷贝到用户空间(同步)。

优点:可以通过一个阻塞对象,等待多个socket上的数据,即通过一个线程监听多个I/O,哪个有数据来了,就交给线程来处理;可以避免频繁地进行阻塞和唤醒操作,减少系统调用的次数,从而大幅降低CPU占用率和内存开销。此外,由于可以同时处理多个I/O事件,还可以提高程序的并发性和吞吐量,适用于高负载和高并发的应用场景。 缺点:需要两次系统调用,在连接数少时性能不是很高。

(4) 同步信号驱动模型

在信号驱动式 I/O 模型中,内核通过信号量标识数据是否准备好(非阻塞),数据准备好后,由线程将数据拷贝到用户空间(同步)。 请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型?

优点:线程完全没有阻塞,可以提高资源利用率; 缺点:也需要两次系统调用,并且在并发量大时,信号量太多,也会影响性能。

(5) 异步I/O模型

在异步模型中,不再区分阻塞或非阻塞,因为完全由内核去完成等待数据准备以及将数据拷贝到用户空间的过程,线程会收到IO操作完成的通知。 请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型? 优点:与同步非阻塞相比,实现简单;与同步阻塞相比,减少了高并发下的线程数。 缺点:为了实现真正的异步I/O,操作系统需要做大量的工作,例如Linux中的AIO以及Windows中的IOCP。

参考这篇文章

3. 线程模型

上边我们介绍了几种I/O模型,不同的I/O模型肯定对应了不同的线程使用方式,即所说的线程模型:

(1) 传统阻塞I/O服务模型

请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型? 该线程模型对应阻塞式I/O,每个连接都需要独立的线程来处理,在高并发的场景下会创建大量的线程,资源占用大;如果当前线程暂时没有数据可读,那么就一直阻塞住,造成线程资源浪费。

(2) Reactor模型

针对传统阻塞I/O的缺点,比较常用的有两种解决方案:

  1. 基于I/O多路复用模型: 多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需通过阻塞多个线程来等待连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理;
  2. 基于线程池复用线程资源: 不需要给每个连接创建线程,而是将连接完成后的业务处理任务分配给线程池中的线程进行处理,一个线程可以处理多个客户端的业务。

也就是说,I/O多路复用 + 线程池,就是Reactor模型的基本设计思想。其中,I/O多路复用通过阻塞一个线程来实现多个socket的监听,又通过线程池来实现线程的复用。 请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型?

Reactor模型中有三个关键角色:

Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应;

Acceptor:Acceptor用于处理连接事件,收到事件后创建handler来处理后续读写事件;

Handler:Handler用于处理实际的读写事件,Reactor 通过调度适当的handler来响应 I/O 事件。

也就是说,Reactor就是那个对多个socket进行监听的线程,并且将事件分发给不同的Handler来处理。

根据 Reactor 的数量和线程数量的不同,又分为 3 种典型的实现:

1) 单Reactor单线程

请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型?

Reactor对象通过select监控读写任务事件,收到事件后通过dispatch进行分发; 如果是建立连接的事件,则由Acceptor处理连接请求,然后创建一个Handler对象来处理后续读写事件; 如果不是建立连接的事件,则Reactor会直接分发给对应的Handler来响应; 由Handler这一个线程完成所有的Read -> 业务处理 -> Send的完整业务流程。

这里可以看到,单线程的Handler很容易成为性能瓶颈,不能充分地利用多核CPU的处理能力。

2) 单Reactor多线程

请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型?

这种模型和单Reactor单线程的区别是,Handler只负责响应事件,而不做具体业务; 具体业务是交由线程池去处理的,处理完成后返回结果给Hander,Handler再将结果返回给客户端。 也就是说,Reactor线程用于处理Read和Send,Hander负责具体的业务处理。

这种模式虽然可以充分利用多核CPU的处理能力,但是在高并发的场景下,单线程的Reactor也会成为性能瓶颈。

3) 多Reactor多线程

请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型? 也可以称为主从Reactor多线程,顾名思义,其Reactor有一个主线程和多个子线程。 Reactor主线程只需要接收新连接,由子线程去完成后续的处理。

这种模式在许多项目中得到了广泛的使用,例如Nginx、Memcached以及Netty。

(3) Proactor 模型

在Reactor模型中,读写操作都需要应用程序同步操作,所以Reactor是同步模型。 如果把I/O操作改为异步,即交给操作系统来完成就能进一步提升性能,就是异步模型Proactor。 请解释:同步/异步?阻塞/非阻塞?I/O多路复用?Reactor模型? 具体可以看这篇文章

4. 总结

现在来简单回答下文章标题中的问题

同步/异步和阻塞/非阻塞,在不同的维度下的意义不同:

在消息通信的维度下,同步和阻塞以及异步和非阻塞为同义词,根据发送方和接收方的不同,可以分为阻塞式发送(同步发送)、非阻塞式发送(异步发送)、阻塞式接收(同步接收)和非阻塞式接收(异步接收),不同类型的发送方式和接收方式可以自由组合。

在I/O的维度下,阻塞/非阻塞表示在等待数据准备时线程是否被阻塞挂起,同步/异步表示是否由线程来进行数据从内核到用户空间的拷贝。

I/O多路复用是一种I/O模型,它通过阻塞一个线程,来实现对多个socket的监听。其他常见I/O模型还有同步阻塞、同步非阻塞、同步信号驱动以及异步I/O。

Reactor模型是一种同步非阻塞线程模型,它的基本设计思想是I/O多路复用 + 线程池。其中,I/O多路复用通过阻塞一个线程来实现多个socket的监听,又通过线程池来实现线程的复用。其他还有传统阻塞式I/O以及Proactor模型(异步模型)。

到最后,再抛出一个问题:

说了这么多,这些跟BIO/NIO的关系又是什么? 请看下文:《BIO与NIO的区别?》

转载自:https://juejin.cn/post/7244820297278390329
评论
请登录