c++ socket线程池_「Java」 - 多线程八 & 线程模型

一、常用线程模型

较早处理网络请求时,是使用一个Accept线程来轮询完成TCP三次连接的socket,然后每次接收到一个socket就开启一个线程进行处理。而线程是宝贵的资源,一个请求对应一个线程进行处理的模式,会很快耗尽系统资源。

62c83c963c62f35f130305751e04ed87.png
public 

二、Tomcat请求的BIO线程模型

Tomcat中BIO的线程模型本质是使用一个线程来接受连接请求,然后使用一个线程池来处理所有的请求,使用有限的线程来处理大量的请求,提高线程的复用性。

3271712b33d4a068c6155ce18db264ae.png

具体接收客户端的还是使用单个Accept线程,Accept线程接受一个完成TCP三次握手的socket后,把socket封装为一个任务,投递到线程池中进行异步处理,然后接受线程就继续阻塞到监听socket的accept方法处等待下一个连接的完成。

protected 

BIO模式中Acceptor线程用来接受完成TCP三次握手的连接套接字,然后封装连接套接字为任务后,投递到线程池进行处理。

protected 

封装socket为SocketProcessor任务后投递到线程池进行处理。

三、Tomcat请求的NIO线程模型

BIO线程模型使用线程池的方式来处理请求,但是本质上还是一个请求一个线程来处理,比如线程池最大线程为10个,那么当同时接受100个请求的时候,10个线程同时分别处理一个请求,假设Tomcat设置的最大连接数大于200,那么这时候有90个请求是被放到线程池的队列里面缓存起来的,等线程池线程有空闲时再从队列里面获取连接进行处理。

Tomcat NIO线程模型是使用Java NIO来异步进行处理。

125c876f7f7cc37a73b8c5f6746b98c6.png
  • Acceptor是套接字接受线程(Socket acceptor thread),用来接受用户的请求,并把请求封装为事件任务放入Poller的队列。
  • Poller套接字处理线程(Socket poller thread),每个Poller内部有一个自己独有的队列,并且有一个selector。Poller线程则是用来从自己的队列里面获取具体的事件任务进行执行,具体是把接受到的连接套接字注册到该Poller管理的selector上,一旦一个套接字注册到了一个Poller的selector上,那么其一生就绑定到了该poller上,不会再改变。Poller线程还负责轮询注册到自己管理的selector上的链接套接字的读写事件,当有读写事件的时候,就会把读写事件具体交给后端线程池进行处理。
  • 可知NIO线程模型又进一步提高了线程复用度,这种模型下使用一个poller线程来管理多个连接套接字的读写事件(BIO中每个socket的读写事件都对应一个线程来处理),并且NIO中并没有一开始就分配线程池中线程给连接socket,而是等Poller线程发现有socket有读写事件时才分配线程。
protected 

上面代码与BIO模式下的比较类似,不同在于NIO重写了setSocketOptions方法。

protected 

把socket转换为了Channel,然后注册到某一个Poller的队列里面。

public 

其中events的定义:

protected ConcurrentLinkedQueue<Runnable> events = new ConcurrentLinkedQueue<Runnable>();

到这里完成TCP三次握手的socket套接字被注册到了某一个Poller管理的队列里面了。

Poller本身是一个线程,下面我们看看run方法如何从队列里获取连接套接字,并将其注册到自己管理的selector上。

public 

从Poller自己管理的队列events中获取一个任务,如果存在任务,则执行任务的run方法。

public 

由以上代码可知,这里把连接socket注册到了poller线程管理的selector上。

Poller线程还做了一件事情,就是监控注册到自己管理的selector上的socket的读写事件,当有事件发生时,就把事件转换为任务放入线程池执行。

public 

Tomcat中NIO模式的使用并没有最大化利用NIO的性能,例如监听套接字还是使用的阻塞模式,只是接受的连接套接字使用非阻塞模式,另外鉴于Servlet规范的约束,也限制了NIO的性能。

在Spring 5.0中出现了一种平行于Servlet技术栈的新Web技术-WebFlux,WebFlux底层使用Reactor&Netty,最大化地利用了Netty的异步高性能来处理网络请求。

四、Netty的Reactor线程模型

Netty作为高性能异步网络通讯框架,其应用范围越来越广泛,比如Dubbo、RocketMQ、SOFA、Zuul、WebFlux等都有Netty的身影。

Netty之所以能提供高性能网络通讯,其中一个原因是它使用Reactor线程模型。在Netty中每个EventLoopGroup本身是一个线程池,其中包含了自定义个数的NioEventLoop,每个NioEventLoop是一个线程,并且每个NioEventLoop里面持有自己的selector选择器。

在Netty服务器端持有两个EventLoopGroup,其中boss组是专门用来接收客户端发来的TCP连接请求,worker组是专门用来具体处理完成三次握手的连接套接字的网络IO请求。

f3e4ab32c371df9bf18a94552856c43e.png
public 
  • 代码(1.1)创建主从Reactor线程池,其中bossGroup线程池线程个数为1,用来接收客户端发来的TCP请求;workerGroup线程池线程个数默认为内核CPU个数*2,用来具体处理IO相关操作。
  • 代码(1.2)创建启动类ServerBootstrap实例,用来设置客户端相关参数 ,其中
    • 代码(1.2.1)设置创建的线程池;
    • 代码(1.2.2)指定用于创建客户端NIO通道的Class对象,这里为NioServerSocketChannel;
    • 代码(1.2.3)设置客户端套接字参数,这里是设置SO_BACKLOG大小为100,监听套接字在接受客户端请求时会维护两个队列,一个是存放已经完成TCP三次握手的套接字的队列,一个是存放还没有完成三次握手的套接字的队列,这个backlog就是两个队列大小之和;
    • 代码(1.2.4)设置日志handler;
    • 代码(1.2.5)设置用户自定义handler,这里在管线里面添加了用户自定义的NettyServerHandler。
  • 代码(1.3)绑定监听端口,并且等待完成。
  • 代码(1.4)同步等待服务端套接字关闭。
  • 代码(1.5)优雅关闭创建的Reactor线程池。

相比Tomcat中NIO模式中监听套接字是同步的,Netty中的boss监听套接字是异步处理连接请求的。

TIPS:通常bossGroup只需要设置为1即可,因为ServerSocketChannel在初始化阶段,只会注册到某一个eventLoop上,而这个eventLoop只会有一个线程在运行。

当有多个Server Bootstraps共享同一个NIOEventLoopGroup的时候,设置NIOEventLoopGroup为多个线程比较好,这是因为当你在多个监听套接字监听服务连接时,每个监听套接字可以绑定到NIOEventLoopGroup中不同的EventLoop上,同时达到多个监听套接字并行处理的效果。

而workerGroup,为了充分利用CPU,同时考虑减少线程上下文切换的开销,通常设置为CPU核数的两倍,这也是Netty提供的默认值。

五、Dubbo的线程模型

Dubbo默认的底层网络通讯使用的是Netty,服务提供方NettyServer使用两级线程池,其中EventLoopGroup(boss)主要用来接受客户端的连接请求,并将它们分发给EventLoopGroup(worker)来处理,boss和worker线程组称为IO线程。

一般来说,如果服务端的逻辑能迅速完成,并且不会发起新的IO请求,那么直接在IO线程上处理会更快,因为这减少了线程池调度。但如果处理逻辑较慢,或者需要发起新的IO请求,比如需要查询数据库,则IO线程必须派发请求到新的线程池进行处理,否则IO线程会被阻塞,将导致不能接收其他请求。

根据请求的消息被IO线程处理还是被业务线程池处理,Dubbo提供了下面几种线程模型。

195d35d7125b917177e9004bd4cfa6bf.png
A、all:(AllDispatcher类)

A、all:(AllDispatcher类)所有消息都派发到业务线程池,这些消息包括请求、响应、连接事件,断开事件,心跳等。

3ba2efd902c1dc11b68a2ffa87e83d36.png
B、direct:(DirectDispatcher类)

B、direct:(DirectDispatcher类)所有消息都不派发到业务线程池,全部在IO线程上直接执行。

fbd34efbdb22582c1df26433b10e34cd.png
C、message:(MessageOnlyDispatcher类)

C、message:(MessageOnlyDispatcher类)只有请求响应消息派发到业务线程池,其他连接断开事件、心跳等消息,直接在IO线程上执行。

b9d92630d9741b6d86111dade577d961.png
D、execution:(ExecutionDispatcher 类)

D、execution:(ExecutionDispatcher 类) 只把请求类消息派发到业务线程池处理,但是响应和其他连接断开事件,心跳等消息直接在 IO 线程上执行,模型如下图所示。

9c6345631d5cdae21bdeae7bb4874f4b.png
E、connection:(ConnectionOrderedDispatcher 类)

E、connection:(ConnectionOrderedDispatcher 类) 在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其他消息派发到业务线程池处理,模型如下图所示。

all这种线程模型的代码, AllDispatcher 对应的handler代码。

public 

所有事件都直接交给业务线程池进行处理了。
另外Dubbo提供了几种业务线程池供开发选择,扩展接口ThreadPool的SPI实现有如下几种:

  • fixed:固定大小线程池,启动时建立线程,不关闭,一直持有( 默认)。
  • cached:缓存线程池,空闲一分钟自动删除,需要时重建。
  • limited:可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然带来大流量引起的性能问题。

其中fixed策略对应扩展实现类是FixedThreadPool。

public 

可知使用ThreadPoolExecutor创建的核心线程数=最大线程池数=threads的线程池。

Dubbo线程池扩展,这些扩展可以满足绝大部分的需求,但是可以根据需要进行扩展定制。

六、Leader-Follower线程模型

IO线程模型一直在演化,由最开始的单线程模型,到BIO方式的单线程接受请求,线程池线程里面某个线程具体处理单个请求的读写事件,再到NIO的单线程接受请求,线程池里面的单个线程可以处理不同请求的读写事件。其实还有个Leader-Follower线程模型,它的出现是为了解决连接接受线程和业务处理线程池中线程上下文切换以及线程间通信数据拷贝所带来的开销,并且不需要维护一个队列。

2ac616d57a228ad08b170de80f28aaf4.png

在Leader-Follower线程模型中每个线程有三种模式:Leader、Follower和Processing。

在Leader-Follower线程模型中,一开始会创建一个线程池,并且会选取一个线程作为Leader线程,Leader线程负责监听网络请求,其他线程为Follower处于waiting状态。当Leader线程接受到一个请求后,会释放自己作为Leader的权利,然后从Follower线程中选择一个线程进行激活,新激活的线程被选择为新的Leader线程作为服务监听,然后老的Leader则负责处理自己接受到的请求(现在老的Leader线程状态变为了Processing),处理完成后,状态从Processing转换为Follower模式。

这种模式下接受请求和进行处理使用的是同一个线程,这避免了线程上下文切换和线程通讯数据拷贝。

在Java开源框架中很少看到这种线程模式的使用,但是在JUC包DelayQueue的实现中却有着Leader-Follower线程模型的思想存在。

public 

也就是获取元素的线程要在队列头部进行等待,为了最大化减少不必要的等待时间,DelayQueue在获取元素的时候借鉴了Leader-Follower 的思想。当一个线程调用队列的take方法变为Leader线程后,会调用条件变量available.awaitNanos(delay)等待delay时间,但是其他线程(Follwer线程)则会调用available.await()进行无限等待。等Leader线程延迟时间过期后,Leader线程会退出take方法,并通过调用available.signal()方法唤醒一个Follwer线程,被唤醒的Follwer线程被选择为新的Leader线程。

具体代码实现DelayQueue的take方法。

public 

如上代码,首先获取独占锁lock,假设线程A第一次调用队列的take()方法时队列为空,则执行代码(1)后first==null,因此会执行代码(2)把当前线程放入available的条件队列阻塞等待。

如果线程A调用队列的take()方法时队列不为空,则会通过delay= first.getDelay(TimeUnit.NANOSECONDS)获取队头元素还剩余多少时间就过期,如果剩余时间delay<=0说明已经过期,则直接返回过期元素。

否则,如果leader==null,说明线程A是第一个调用take方法的,则选取线程A为Leader线程,然后执行代码(6)线程A就会被阻塞挂起delay时间,这时候如果有其他线程调用了take方法,则直接调用代码(4)永久挂起(直到收到signal信号),这些线程其实就成为Follower线程了。

Leader线程A等delay时间到了后,会从代码(6)返回,然后设置leader为null,循环一次后,执行代码(3)从队头拿出过期元素,然后执行代码(7)激活一个因为调用代码(4)而被阻塞的线程,这时候Follower线程中的一个就会被激活,成为新Leader。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值