一、多线程调度瓶颈分析
之前加强版的调度流程,在之前的调度流程当中,有一个专属的Accepter的线程,用来做我们的连接。当有新的连接的时候,将它们添加到连接队列。在连接队列中可以分别进行输入、输出的selector的线程,这个线程命名为Processor。当数据就绪的时候,会把scoket通道拿出来,再把任务解析出来,丢到线程池当中去。在线程池中处理对应的reader或者writer。
想法非常简单,当有事件的时候,就把事件丢给线程池去完成,尽可能地发挥cpu的能力。但是在这个过程中遇到非常多的问题,在这之前有20多个线程,在这20多个线程中,真正工作的只有2个。一个是读,一个是写,在某个时刻,只有这两个,在这个时刻过去之后,才会有另外的线程又来运行。所以线程池的最大本质并没有发挥它的作用,这主要是因为相互在竞争锁导致的性能低下。
二、死锁问题
此时有X、Y两个线程,有两个不同的资源A、B,红点代表我们的资源。
如果线程X优先去拿资源A,此时B也被Y线程拿到,如果资源是被独占的,那么需要用锁把它保护起来。此时分别会给A、B资源加上锁,A资源的锁被线程X所持有,B资源的锁被线程Y所持有。此时线程X要想获取资源B,Y线程要想获取资源A,它将获取不到。为什么呢?此时线程X想要获取到的资源B已经被线程Y获取到了,线程Y所要获取到的资源A已经被线程X获取到了,它们两者相互等待。若是在这样的情况下,就会出现两个线程都被阻塞掉,都被等待了。两个线程等待,而且是一个死等待,X永远等待资源B释放,而Y永远等待资源A释放。
若我们的逻辑是X线程想要释放资源A必须拿到资源B做一定的操作之后才会释放资源A的话,这时就会出现问题,线程Y不释放资源B,线程X永远永远得不到后续的运行。如果线程Y也是同样的逻辑,那么就会出现线程Y永远等待线程X释放,线程X永远等待线程Y释放,两者相互等待,出现死锁。这是死锁的问题,但是我们当前的问题并不是死锁的问题,而是当前的锁被占用之后,其它的线程一段时间之内无法得到运行的问题。
三、多线程调度瓶颈
有一个静态资源,也是一个需要锁去保护的资源,这个资源就是selector,这个selector有可能是selector的一个输入,也可能是selector的一个输出,不管怎样,每个selector都有一个对应的selector thread。这个Selector Thread永远在循环selector里面的一个selector方法。
此时有一个线程池,线程池中有非常多得到子线程,这些子线程都想获取到selector这个变量,因为它要注册或者反注册。一旦要注册或者反注册,那么就需要对selector进行唤醒,然后再进行后续的逻辑,涉及到的方法有registerChannel()、select()、wakeup(),而这些操作我们使用一个AtomicBoolean值来保护。由于这个值,导致了在某一个时刻,在Selector Thread获取到资源的时候,其它的子线程无法进行注册或解绑。如果子线程想要注册或解绑,一定要让当前的Selector Thread将资源释放掉。如果想要释放,就需要调用wakeup(),让它释放掉后子线程马上拿到锁,然后子线程再处理其它事情。子线程处理完毕后再通知Selector Thread, Selector Thread再去循环。而在循环的过程中,也有可能被其它的线程所持有。另外的线程也有可能会出现想要注册、解绑,当连接量越多,这个几率就越大。而越多,导致了竞争越激烈,越激烈就导致了线程之间的切换。从SelectorThread切换到某一个子线程,或者是某一个子线程切换到SelectorThread都是一样的。资源之间的切换,浪费的是cpu。cpu其实大部分时间消耗到了线程切换上面,而不是真正地干事情,所以就导致了性能低下。
而之前进行了一下简单的优化,将线程数量减小,然后直接进行输出,而不是进行注册后调度输出,这样就得到了线程性能上的调优。
四、发送数据调度优化
有一个静态资源,就是selector,Loop Thread对其进行循环,之后有一个主线程,是我们自己自定义的要发送数据的子线程。
子线程如果要发送数据,需要先注册一个输出,而注册输出涉及到一个锁的获取及释放的过程。这个过程详细可以理解为如下,首先会把selector唤醒,也就是让当前的Loop Thread进行一个暂停,等待当前线程后续执行完之后再进行循环。子线程调用wakeup()之后拿到锁,然后进行注册,注册完毕后通知注册完成,然后Loop Thread继续循环。当Loop Thread循环到子线程就绪可以进行输出后,通知子线程可以进行处理,此时的操作就是handSelection,而handSelection内部就是selectionKey,SelectionKey拿出之后就可以拿到当前的通道,而拿到通道之后可以在通道中做一些事情,而这些事情不是当前的Loop Thread去做,而是会把这些操作丢到一个线程池中,让线程池来做这些操作,即向线程池中丢一个任务。这个任务真正被执行的时候,才是它被写数据的时候。当写完的时候,会判断当前任务还有没有,如果还有,会再次尝试唤醒、注册,再去处理输出的流程,这是要闭环的。
这是之前的流程,这个流程错在哪里呢?
错在想要进行数据的输出是非常迫切的,就是想进行一个数据的输出,但是中间执行了过多的步骤,唤醒->注册->就绪->放入线程池->执行输出任务,但是往往只有进行输出的操作,这些数据都是可以输出出去的。也就代表了当前这个socket,其实90%的可能都是已经就绪的,它就可以进行数据的输出。若发送100字节的数据,可能发送了99个字节,还有1个字节发送不出去,有这样的情况,此时再来进行注册操作是可以的。但是若本身可以发送99个数据的情况下,就进行了前面这么多的步骤再去发送数据,把最后1个字节还进行这样的操作,那就相当于2遍调度消耗。
五、优化之后的模型
也是一个Selector Thread进行循环,子线程直接进行输出,若输出成功,则代表数据发送完成,发送下一条数据。如果没有发送成功,则代表当前的socket可能没有就绪,则进行后续的唤醒->注册->循环->处理->丢到线程池->取出任务进行执行,此时做这个操作是可以的。输出之后再执行write输出操作,这就是简化之后的操作。
这个操作非常明确,我优先做的事情就是输出,我先尝试一下能不能进行输出,如果不能输出数据,也耗费不了多长时间。如果能够输出数据,则免去了后续的一系列流程,所以就让我们的调度得到了优化。
六、总结