目录
一:服务器和客户端之间通信
进程【数据共享问题,需要IPC技术,代码繁多】
线程【线程的数量会随着客户端数量增加而增加,计算机承载线程数量存在上限】
epoll【epoll底层原理是将所有客户端在服务器上面代表客户端的acceptfd文件描述符存储下来,在内部实现一种事件驱动的方式,有客户端发送,都会以事件的方式进行提醒,提醒当前的进程有fd文件描述符产生了某个操作,然后将文件描述符转为到当前的进程当中去执行,这样就不用开进程也不用开线程了】
epoll弊端:事件产生的时候一定会产生先后顺序,谁先发起事件,epoll会先读那个事件 ,存在有事件队列(需要排队),打个比方,就比如说有十个人先注册,那么第十一个人想要登录的人就需要等这前面的十个人注册过后才可以进行登录。客户体验感非常差。由此可见,线程仍然是有非常重要的作用的。线程的创建销毁是比较耗费CPU的算力的,如果频繁创建销毁线程,资源就是无端消耗。
如果以这样线程启动销毁使用就是非常浪费资源的,因此考虑到是否可以让线程实现复用。
采用线程池的方式,只做一次启动和一次销毁 。
线程池:一次性创建一定数量的线程,当线程池类销毁的时候再一次性销毁所有线程
在资源上合理利用。 线程有空闲和忙碌两个状态。
二:线程池基本概念
•线程池是【预先】创建线程的一种技术。线程池在任务还没有到来之前,创建一定数量(N)的线程,放入空闲队列中。这些线程都是处于阻塞(Suspended)状态【阻塞状态不占用资源】,不消耗CPU,但占用较小的内存空间。•当新任务到来时,缓冲池选择一个空闲线程,把任务传入此线程中运行;如果缓冲池已经没有空闲线程,则新建若干个线程。当系统比较空闲时,大部分线程都一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。
三:线程池组成部分
•线程池类–维护工作者线程队列(包括空闲与忙碌队列)–维护一个任务队列(先后,先到先得)–维护一个线程池调度器指针(线程执行函数 函数指针)•线程池调度器(本身也是一个线程)–负责线程调度–负责任务分配•工作者线程类(线程池中的线程类的封装)•任务队列•任务接口(实际的业务逻辑都继承自该接口)
四:线程池执行原理
•线程池类至少提供三个接口,初始化线程池、销毁线程池、添加任务接口•初始化线程池–开启线程池调度器线程–预先创建N个线程(由线程调度池器类负责创建线工作者线程),放入空闲线程队列–指定最大的忙碌状态的线程数•销毁线程池–释放空闲队列中的线程与工作状态中的线程–释放调度器线程•添加任务–添加一实际任务,但是并没有立刻运行该任务,只是放入任务队列,由线程池调度器从任务队列获取该任务,并从线程池中获得一个线程来运行该任务,这里实际上是一种生产者消费者模型。
•线程池调度器包含创建空闲线程、销毁空闲线程接口•线程池调度器本身也是一个线程,主要负责任务调度与线程调度,其工作过程大致如下:–从任务队列获取任务,如果队列为空,阻塞等待新任务到来–队列不为空,取出该任务,从空闲线程队列取一线程,如果为空,判断工作者线程数是否达到上限,如果没有,则创建若个空闲线程,否则等待某一任务执行完毕,并且该任务对应的线程归还给线程池–获得空闲工作者线程,将任务交给工作者线程来处理,工作者线程维护一任务指针,这里只要该指针指向任务,并且唤醒线程–判断空闲工作者线程数是否超过最大工作者线程数,如果超过,销毁(空闲线程数-允许最大空闲线程数)个线程
•任务接口是一个抽象类,只有一个虚函数run方法,执行的是实际的业务逻辑•工作者线程维护一任务指针,工作者线程的任务主要是运行任务对象的run方法。•当线程池调度器调度一个工作者线程后,就唤醒工作者线程,并调用run方法来执行实际的业务逻辑,当run方法执行完毕,即业务逻辑处理结束,将工作者线程归还到空闲线程池队列,而不是销毁。这样线程池调度器下一次就有机会调度到该工作者线程。
线程执行原理
没有任何任务执行的时候,线程阻塞状态
假设有一个注册业务
业务传递给线程,去唤醒
线程唤醒之后从空闲出来到忙碌 列表(链表增删)
在业务顺利执行完后,再从忙碌回到空闲
考虑情况有:
所有线程处于忙碌时,又来了一个业务
业务进入之后发现没有空闲线程
此时就直接创建一个新的线程,进入忙碌
业务成功执行后,仍然会忙碌->空闲
若是所有线程 忙碌->空闲,没有任何的任务,所有线程都处于空闲。最初的线程只有六个,但是现在多了一个
此时将多出来的线程销毁
队列(queue先进先出)和链表(list频繁增删):
五:线程池设计注意点
• 任务的通用性• 线程创建和销毁策略• 任务分配策略
六:任务的通用性
•不同的业务解决方案有各自独特的任务处理方法,任务的划分上也就千差万别。为了使得在处理任务对象的时候达到一定程度的通用性,任务对象的设计上必须与实际任务的处理逻辑完全无关。从任务执行的角度看,任务不过是处理流程的一次或者多次执行的过程,可以这样来定义如下任务接口
•任务在其需要的时候才创建。任务的创建通过new操作,动态创建具体的任务对象,然后传入线程池,由线程池自动分配线程来执行此任务。•任务是否执行完毕由其自身来决定。一个未知任务什么时候执行完毕是不可能预测的,必须任务本身来决定。这个策略通过,run()的返回值来实现。当工作线程执行一次任务时,如果返回值为true,表示任务执行完毕,就用delete操作销毁此任务;如果返回值为false,表示任务需要执行的工作并未完成,继续执行此任务。•这样的策略,使得在设计新的任务处理流程的时候,不需要过多的关心任务的接口规范,只需要在新任务类的构造函数中初始化各种资源,在新任务类的析构函数中回收资源,在run()方法中实现主要的处理逻辑,那么新的任务类即可在线程池中执行。
七:线程创建和销毁策略
•在缓冲池刚刚建立时,线程池中有一定数量(N1)的已创建好的线程,这样可以使得新任务可以及时的得到执行。估计出平均情况下,一次业务产生的任务数量N2。那么N1应该是N2的整数倍,N1=N2×n1•在线程缓冲池中的所有线程都处于繁忙状态的时候,线程池就会创建新的线程,设创建N2个。由以上分析,为了减少由于线程不够而再创建线程的概率,N3也应该是N2的整数倍,N3=N2×n2。•当服务器业务减少,出现大量线程闲置的情况,就应该销毁一部分线程。很显然,这里应该使用超时策略,当某些线程在超过时间T仍然处于闲置状态,就销毁一部分空闲线程。设销毁N4个空闲线程,为了减少由于线程不够而再创建线程的概率,N4也应该是N2的整数倍,N4=N2×n3。当然,为了使得新任务及时得到处理,即使服务器一直处于空闲,也应该保留N1个线程。
八:任务分配策略
•在业务处理中,会有各种各样的任务对象,这些业务对象对系统资源的使用也不同。这些任务,无论其空间复杂度如何,从线程执行任务这一角度来看,应该关心的主要是时间复杂度。•线程缓冲池在接收到新任务的时候,首先要寻找空闲线程,传入新任务,然后执行任务,最后还要删除任务,置空闲线程的标志。寻找空闲线程、传入任务、最后的清理工作,这些都是为了执行任务而产生的额外开销,如果所执行的任务大多数都是轻量级任务,那么额外开销带来的资源浪费就显得很突出了。为了解决这个问题,可以给一个线程传入N5个轻量级任务,这一个线程依次执行N5个轻量级任务,由于都是在很短时间内完成,并不影响任务响应的及时性。显然,N5≥1。