天天看点

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

作者:Java热点

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

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

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

(1) 消息通信维度

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

阻塞式发送(同步发送):发送方发送消息后会被阻塞,直到消息被接收方接收到;

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

阻塞式接收(同步接收):接收方获取消息时会被阻塞,直到发送方的消息到达;

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

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

参考这篇文章。

(2) I/O维度

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

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

  1. 等待数据准备

    阻塞:线程一直阻塞等待数据;

    非阻塞:线程发送请求后,不等待数据,通过轮询/信号量等方式去获取数据是否准备好了。

  2. 将数据从内核拷贝到用户空间进程中

    同步:准备好数据后,线程需要自己将数据从内核拷贝到用户空间,然后处理数据;

    异步:准备好数据后,系统内核把数据拷贝到用户空间,然后通知相应线程进行数据处理。

用个烧开水的例子来说明:

我开始烧水之后,一直等待着水开,就是阻塞;

我开始烧水之后,去干别的事情了,过一会回来看一下水有没有烧开,就是非阻塞;

水开了之后,需要过我过来关火,就是同步;

水开了之后,烧水壶自动断电,就是异步。

因此,根据同步/异步和阻塞/非阻塞的不同,就引申出了不同的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 有所不同。

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

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

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

缺点:需要两次系统调用,在连接数少时性能不是很高。

(4) 同步信号驱动模型

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

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

优点:线程完全没有阻塞,可以提高资源利用率;

缺点:也需要两次系统调用,并且在并发量大时,信号量太多,也会影响性能。

(5) 异步I/O模型

在异步模型中,不再区分阻塞或非阻塞,因为完全由内核去完成等待数据准备以及将数据拷贝到用户空间的过程,线程会收到数据到达的通知。

请解释:同步/异步?阻塞/非阻塞?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 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;

Handler:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。

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

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

  1. 单 Reactor 单线程;
  2. 单 Reactor 多线程;
  3. 主从 Reactor 多线程。

(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模型(异步模型)。

继续阅读