一、 线程模型
1.1 介绍
1.目前的线程模型
- 传统阻塞I/O线程模型
- Reactor线程模型
Reactor线程模型分类
- 单Reactor单线程
- 单Reactor多线程
- 主从Reactor多线程
Netty线程模型,是基于主从Reactor多线程模型的进行了改造而产生的
1.2 传统阻塞I/O线程模型
最早的模型是中,是用一个while循环来监听端是否有新的链接。如下图代码所示:
while(true){
socket = accept(); //阻塞,接收连接
//*** handle 操作
socket.getInputStream().read();//读取数据
byte[] data;//处理业务
socket.getOutputStream().write(data);//写入结果
}
这种一个很大的问题,就是一旦handle阻塞来。那么后面的链接请求就全部比阻塞来。导致服务的吞吐量特别的低。为了防止这种阻塞问题,有出现了了一个线程处理一个链接的模式,如图所示
模型的特点
- 采用阻塞IO模式获取输入的数据
- 每个链接都需要独立的线程完成数据的输入、业务处理和数据返回
大致的伪代码如下:
while(true){
socket = accept(); //阻塞,接收连接
new Thrad(new Handle(socket)).start ; //读取数据、业务处理、写入结果
}
class Handle implements Runable{
void run{
socket.getInputStream().read();//读取数据
byte[] data;//处理业务
socket.getOutputStream().write(data);//写入结果
}
}
模型导致的问题
3. 当并发数很大的时候,会创建大量的线程,占有很大的系统资源 (比较线程是稀缺资源)
4. 线程创建后,如果没有数据可读,该线程就会阻塞在read操作上,操作线程资源的浪费
1.3 Reactor线程模型
基本概述
- 基于I/O复用模型,多个链接对象共用一个阻塞对象,应用程序只需要在一个对象上阻塞等待,无需阻塞等待所有的链接。
- 基于线程池复用线程资源,将链接完成的业务处理交给线程池处理
如下图所示
说明: - reactor模式,通过一个或多个输入同时传递给服务器的模式(基于事件驱动)
- 服务器处理器 eventDispatch(也就是reactor)在一个单独的线程中进行,负责监听和分发事件,具体的业务处理有它分派给对应的处理线程。因此Reactor模式也叫Dispatch模式
- Reactor模式使用了IO复用监听事件,收到事件后,分发给某个线程。
1.4 单Reactor单线程模型
- selector 是NIO中的I/O 复用模型的标准网络api,可以实现应用程序通过一个阻塞对象监听多路链接请求
- Reactor对象通过Selector监控客户端的请求事件,然后进行分发
- 如果是建立连接事件,则有Acceptor通过accept,注册一个SelectionKey.OP_READ的连接channel
- 如果不是建立连接事件。则Reactor会分发调用不同的Handler来详细
- Handler会完成 read -> 业务处理 -> send的完整业务流程
服务端用一个线程通过多路复用搞定所有的IO操作(包括连接、读写等)。逻辑清晰。但是当来连接请求过多的时候,将无法支撑
对于Selector不了解的 可以访问 Netty学习——BIO、NIO、AIO的总结
优点
- 模型简单,没有多线程,进程的通信、竞争的问题。全部都在一个线程中完成
缺点
- 性能问题,只有一个线程,无法发挥多核CPU的新年。Handler在处理某个连接上的业务时,无法处理其他的连接事件,是性能瓶颈
- 可靠性问题,线程意外终止,或者进入死循环,将导致整个系统通信不可用。
使用场景
- 客户端数量不多,业务处理快。比如redis在业务处理的事件复杂度为O(1)的情况
1.5 单Reactor多线程模型
- reactor对象通过Selector监控客户端的请求事件,收到事件之后,通过dispatch进行分发
- 如果建立连接的请求,则由Acceptor通过accept处理请求,并创建一个handler对象处理完成连接的各种事件
- 如果不是连接请求,则由reactor分发调用连接对应的handler来处理
- handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分发给后面的worker线程池来执行
- worker线程池会分配一个空闲的线程去完成真正的业务,并将结果返回给handler
- handler收到响应后,通过send将结果返回给client
优点
- 可以充分发挥多核CPU的处理能力
缺点
- 多线程访问共享数据比较复杂
- reactor需要处理所有事件的监听和响应,单线程情况下,如果在高并发场景下容易出现性能问题
1.6 主从Reactor多线程模型
- Reactor主线程 MainReactor对象通过select监听连接事件,收到事件后,通过Acceptor处理连接请求
- 当Acceptor处理连接事件后,MainReactor将连接分配给SubReactor
- subReactor将连接加入到连接队列进行监听,并创建handler进行各种事件处理
- 当有新事件发生时,subReactor就会调用对应当handler处理
- handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分发给后面的worker线程池来执行
- worker线程池会分配一个空闲的线程去完成真正的业务,并将结果返回给handler
- handler收到响应后,通过send将结果返回给client
- Reactor主线程可以对应多个Reactor子线程
优点
- 父线程和子线程的数据交互简单,职责明确,父线程只需要接收新连接,子线程完成后续的业务处理
- 父线程和子线程的数据交互简单,Reactor主线程只需要吧连接传给子线程,子线程无需返回数据。
缺点
- 编程复杂度比较高
使用场景
这种模型在许多项目中广泛使用,包括Nginx主从Reactor多进程模型,Memcached主从多线程等
2.7 Netty模型
- Netty抽象出两组线程吃,BossGroup专门负责接收客户端的连接,WorkerGroup专门负责网络的读写
- BoosGroup和WorkerGroup的类型都是 NioEventLoopGroup。
- NioEventLoopGroup相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环是NioEventLoop
- NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socker网络通信
- NioEventLoopGroup可以有多个线程,即可以含有多个NioEventLoop
- 每个BossNioEventLoop循环执行的步骤是
- 轮询accept事件
- 处理accept事件,和client建立连接,生成NioSocketChannel 并将其注册到WorkerGroup中的某个Selector上
- 处理任务队列的任务,即runAllTask
- 每个WorkerNioEventLoop循环执行的步骤
- 轮询 read/write事件
- 处理io事件,即read、write事件。在对应的NioSocketChannel处理
- 处理任务队列的任务,即runAllTasks
- 每个workerNIOEventLoop处理业务时候,会使用pipeline(管道),pipeline中包含了channel,即每个pipeline可以获取到对应的管道,管道中维护了很多的处理器