设计时要区分:面向多消息的多线程还是面向多连接的多线程。
一、服务器线程模型分类:
(1)循环模式:只能同时监听一个事件源,来一个连接生成一个线程。这只能是多线程,不能算线程池,需要和controler/worker配合使用。这种模型当客户端连接数快速增长是就会出现性能瓶颈。因为创建线程需要浪费大量的时间。
(2)反应模式(reactor):通过多路复用select,可以同时监听多个事件源。这也只能是多线程,不能算线程池,需要和controler/worker配合使用
(3)Controler/Workers(适合多消息的处理):主控线程预先生成很多线程,并负责事件源,及worker线程的启动和同步。当到来一个连接时,启动一个阻塞队列中的工作线程负责处理该连接。在linux中可以通过条件变量来实现。可能出现的性能问题:controler把工作交给一个worker线程的时候可能需要上下文切换,包括数据拷贝。
(4)Leader/Followers(LF)(适合多连接的处理):所有的线程都是对等的。该模型所有线程会有三种身份中的一种:leader和follower,以及一个干活中的状态proccesser。它的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下次成为Leader。该模型的优点是解决了controler/workers不足,Leader-Followers模型可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。实现方式:让所有线程去竞争一个mutex,得到的成为leader,负责阻塞连接,当接收到连接时,解除mutex,并负责处理该连接。处理完之后又去竞争mutex,并阻塞。是不是太简单。
线程池的作用:提高消息(任务)响应的实时性、提高任务执行的速度。
二、线程池的注意事项
(1)线程池大小。多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与CPU 数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可采用自适应算法来动态调整线程池的大小,以提高CPU 的有效利用率和系统的整体性能。
(2)并发错误。多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避免死锁现象的发生。
(3)线程泄漏。这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。
三、Controler/Workers线程池模型
一个典型的Controler/Workers线程池,应该包括如下几个部分:
1、线程池管理器(ThreadPool),用于启动、停用,管理线程池
2、工作线程(WorkThread),线程池中的线程
3、请求接口(WorkRequest),创建请求对象,以供工作线程调度任务的执行
4、请求队列(RequestQueue),用于存放和提取请求(实现获取请求与插入请求的互斥)
5、结果队列(ResultQueue),用于存储请求执行后返回的结果
线程池管理器:通过添加请求的方法(putRequest)向请求队列(RequestQueue)添加请求,这些请求事先需要实现请求接口,即传递工作函数、参数、结果处理函数、以及异常处理函数。之后初始化一定数量的工作线程,这些线程通过轮询的方式不断查看请求队列(RequestQueue),只要有请求存在,则会提取出请求,进行执行。然后,线程池管理器调用方法(poll)查看结果队列(resultQueue)是否有值,如果有值,则取出,调用结果处理函数执行。通过以上讲述,不难发现,这个系统的核心资源在于请求队列和结果队列,工作线程通过轮询requestQueue获得任务,主线程通过查看结果队列,获得执行结果。因此,对这个队列的设计,要实现线程同步,以及一定阻塞和超时机制的设计,以防止因为不断轮询而导致的过多cpu开销。
例子:条件变量实现线程池模型(不带结果队列)
条件变量实现线程池模型基本原理:工作线程默认情况下是阻塞在 pthread_cond_wait() 系统调用下的,如果有任务到来,我们可用使用 pthread_cond_singal() 来唤醒一个处于阻塞状态的线程,这样这个线程就可以执行 mc_thread_pool_get_task() 来取得一个任务,并调用相应的回调函数。
typedef struct _thread_pool_t
{
pthread_mutex_t queue_lock ;//任务锁