线程池

简单的搭建一个高并发低时延系统

首先声明一点:这里的“高并发”是相对的,相对于硬件而言,而不是绝对的高并发。后者需要分布式来实现,这里不做讨论。本文关注的是单机的高并发。

最近在做一个语音通信系统,要求在线用户2W,并发1K路通话。硬件是两台服务器,酷睿多核,4G内存,千兆网卡(我用过的最好的硬件,负担这些应该问题不大)。

系统的另一个指标是呼叫时延和语音时延。这是这个系统的关键。最终我们的系统拿到用户现场测试的时候,效果可能有点太好,对方测试不大相信。其实降低时延只要几个地方把握好了,应该问题不大的。这里总结一下。

 

1、 整体结构:

整体上采用控制与承载相分离的结构。控制部分负责流程的控制部分,包括流程的建立,处理,语音资源的管理等,是系统核心部分。承载部分主要负责语音处理,包括语音编解码,加解密,转发录音等。这样的好处是:1)降低系统的整体复杂度。2)提高系统的可扩展性。特别是如果用户数上去,这种结构更好扩展。

这在通信中其实就是一个典型的软交换结构。两台服务器,一台负责控制,一台负责承载。控制和承载之间通过网络通信。

控制程序是一个进程,可以管理多个承载程序。

2、 流程:

要降低时延,关键的一点是功能实现流程的设计。要减少不必要的环节和网元间的交互。数据能够一次通知就不要两次交互。必要的时候,为了时延,可以牺牲一点协议的标准性,使用私有协议完成(至少从目前看没有问题,这个系统是一个端对端封闭的系统。)。

3、开发语言:

控制层面使用的python来实现的。控制部分流程逻辑复杂,而python很擅长描述逻辑。本来有点担心python的运行效率,其实没有必要:整个系统的压力在承载,而不在控制部分,控制部分不会有太多的压力;另外,cpu够强悍,时延的瓶颈在I/O。况且,python也重用了我们之前用C实现的协议编解码库。

承载部分用C来实现。

4、 利用多核

利用多进程来利用多核。在承载服务器上,并行跑了两个进程,每个分别处理500路通话。也许线程切换成本更低,但是编程复杂度高。对于线程,我只用最简单的模型。

控制部分没有多进程,似乎利用不能这个服务器的多核,不过目前来看,还不需要,因为现在就能很好的满足需求。

5、 网络通信

承载服务器的压力中很大一部分来自于网络通信。按照我们的功能,1000路语音并发,意味着没20毫秒至少要处理1000个语音包(最恶劣的情况是2000个语音包,包括收发)。

Libevent开源,号称轻量级高性能,而且应用也广泛,也许是个不错的选择。不过在我看来还是有些庞大,很多特性(跨平台,多种通信模型)我都用不上。

更为关键的一点事,linux的epoll接口足够简单,而且非常好用。接口中提供一个参数可以设置用户数据,这样我可以把一些数据包括函数指针放进去,从而很方便的构造一个事件驱动的网络模块。它能够保证代码足够简单。

6、 文件读写

整个系统涉及到文件读写的主要有两块:录音及日志。我们常用的文件操作接口都是阻塞式,进程(线程)会被挂起,等待读写完毕,然后在继续执行。我们都知道,对磁盘的操作要慢很多,所以这个地方是请求时延的一个瓶颈。

异步IO可以解决这个问题。参考资料:http://www.ibm.com/developerworks/cn/linux/l-async/。不过网上看到有人说AIO接口有bug。时间不多,没有时间深入研究,还是保守的放弃了这个思路。新技术有风险,使用需谨慎。

Libeio也应该是一个选择。参考资料:http://rdc.taobao.com/blog/cs/?p=1524,它是用线程池来模拟异步IO。问题是,我们的程序主要是写文件,而且一般不需要知道结果,在这种情况下使用libeio的必要性有多大?

我们最终的方案是参考libeio,直接为承载进程申请了一个线程来负责写文件。主线程负责语音编解码及转发,完全非阻塞,以保证低延迟。涉及的文件写操作,通过接口发送给另外一个线程调用阻塞IO接口来实现。线程间接口很简单,一块要写的内容加一个路径名。

7、 数据库操作

我们的数据库使用的是mysql。和文件读写一样,数据库操作也是请求时延的一个瓶颈。在整个流程中,我们会多次的读写数据。我们的做法是:系统启动后,将运行时用到的数据全部读到内存,后面直接查看内存。好在数据不大,这个工作也简单。如果涉及到数据的新增修改删除。则另外一个线程完成相关操作,再通知主线程更新内存。

最终的结果是,主线程是完全非阻塞的,涉及到阻塞的操作,全部移到另外一个线程中。两个线程不共享任何全局数据,只通过FIFO交互。

这个地方redis也许是可以考虑的一种选择,它的数据保存在内存,读写效率也非常好。不过相对来说还是有点复杂,而且还是nosql,我们的开发人员并不熟悉。“最小惊讶原则”不但适用于程序接口,也适用于系统。

 

经过所有的这些考虑和优化,可以基本达到目标,而且足够简单。

 

如何榨干服务器:

经过上面的一些优化,基本上可以满足用户的需求了。但我知道,还没有完全的利用服务器的能力(包括CPU,IO)。要进一步榨干服务器的能力,可以在承载服务器上将每个进程的处理能力扩大一倍,每个进程处理1000路。也可以考虑再多跑几个进程。

控制服务器没有充分的利用多核,可以考虑在控制服务器也运行两个承载程序。

这样下来,初步估计硬件不提高的情况下,注册用户数至少能够提高到6W,并发呼叫数目至少能够提高到3K。

提高绝对容量和并发:

业务特点不同,通信行业高并发的解决方案和互联网行业可能会有一些区别。可以想一下我们使用的电话系统,就是一个实例。它通过分布在全国各地的一个个用户归属的局端,再配合强大的路由能力,以及端到端之间非常标准的协议来解决。

采用类似的方案也可以提高本系统的容量和并发,不过,目前系统的容量已经可以满足我们公司市场几年内的需求,没有进一步提升的必要,还是保持简单的好。

 

总结:

很多时候,使用开源软件都是一个非常不错的主意,可以避免“重复发明轮子”。而且,它还有一定的诱惑:它可以为你的履历加分。

但是有的时候,你需要的可能并不是“轮子”,想想为一个滑板安装一个汽车轮是什么效果。

 

什么样的方案才是好的方案?

1、 满足现在的需求及未来50%的需求。当一个可预见的需求发生概率超过一半时,为它考虑可扩展时必要的。否则会过度设计而冲击简单性。

2、 保持简单。

 

最后,再次提一下KISS原则,keep it simple and stupid !









=====================================================================================今天能把线程池异步IO搞明白就阿弥陀佛了=============================


本作品采用知识共享署名 4.0 国际许可协议进行许可。转载联系作者并保留声明头部与原文链接https://luzeshu.com/blog/nodesource7
本博客同步在https://cnodejs.org/topic/571618c7e84805cd5410ea26
本博客同步在http://www.cnblogs.com/papertree/p/5405202.html


在上篇博客讲到,网络io通过封装io观察者(uv__io_t),添加到loop->watcher_queue队列。在2.2节中讲到文件异步io不同于网络io,文件异步io把请求操作交给线程池处理,所有线程池的异步io操作统一由一个io观察者来管理,等线程池处理完毕再通过该io观察者告知事件循环(epoll_wait)有异步io操作完成,需要在事件循环的线程执行回调函数。

这篇博客分以下几个部分讲解其中的细节:

1 从文件异步io操作到封装请求交给线程池的过程

2 线程池的原理、相关的系统支持【互斥锁、条件变量】

3 线程池完成io操作后,告知主线程/事件循环的方式 —— 线程池统一的io观察者,及相关的系统支持【管道、事件对象】

4 主线程epoll_wait收到线程池的通知后,回调到文件异步io操作的callback的过程

7.1 文件异步io到线程池

上一篇博客以server.listen(80)为例来讲解网络io,这一篇以fs.writeFile(‘xxx’, function (err, data) {});为例来讲解文件异步io。

js代码到libuv的函数,经历了几个层次(6.2.1节-6.2.4节,“原生js lib模块 -> node C++模块 -> libuv模块”),这几个层次文件io和网络io是类似的,就忽略了。

这里只针对libuv的文件异步io如何封装成请求对象交给线程池。

7.1.1 libuv的文件io请求对象 —— uv_fs_t

看一下libuv的异步读文件代码,deps/uv/src/unix/fs.c:

7-1-1.png<center>图7-1-1</center>

可以看到一次异步文件读操作在libuv层被封装到一个uv_fs_t的结构体,req->cb是来自上层的回调函数(node C++层:src/node_file.cc 的After函数)。

异步io请求最后调用uv__work_submit,把异步io请求提交给线程池。这里有两个函数:

uv__fs_work:这个是文件io的处理函数,可以看到当cb为NULL的时候,即非异步模式,uv__fs_work在当前线程(事件循环所在线程)直接被调用。如果cb != NULL,即文件io为异步模式,此时把uv__fs_work和uv__fs_done提交给线程池。

uv__fs_done:这个是异步文件io结束后的回调函数。在uv__fs_done里面会回调上层C++模块的cb函数(即req->cb)。

<font color=“red”>这里需要注意的是,异步模式下,把uv__fs_work、uv__fs_done当成参数调用uv__work_submit向线程池提交异步io请求,此时io操作的主体 —— uv__fs_work函数是在线程池里执行的。但是uv__fs_done必须在事件循环的线程里被回调,因为这个函数最终会回调到用户js代码的回调函数,而js代码里的所有代码必须在同个线程里面。</font>

7.1.2 线程池的请求对象 —— struct uv__work

来看看uv__work_submit做了什么:

图7-1-2.png<center>图7-1-2</center>

uv__work_submit 把传进来的uv__fs_work、uv__fs_done封装到uv__work结构体里面,这个结构体表示一个线程操作的请求。通过post把请求提交给线程池,post的原理7.2节讲。

看到post函数里面的QUEUE_INSERT_TAIL,把该uv__work对象加进wq链表里面。<font color=“red”>wq是一个全局静态变量。也就是说,进程空间里的所有线程共用同一个wq链表</font>。wq队列的使用在最下面的7.4.2节会用到。

至于通过void* [2]类型的成员变量w->wq去维护一个链表的机制,在6.4节里有介绍。

7.2 线程池的原理 —— 条件变量与互斥锁

7.2.1 条件变量与互斥锁基础

1 互斥锁 —— pthread_mutex_t mutex

系统通过pthread_mutex_t结构、及相关的pthread_mutex_lock()、pthread_mutex_unlock()来对共享资源的请求进行加锁、解锁。

2 条件变量 —— pthread_cond_t condition

系统通过pthread_cond_t结构、及相关的pthread_cond_wait()、pthread_cond_signal()函数来实现线程间等待、通知的机制。

【注意:系统提供的条件变量机制必须结合互斥锁使用,也就是pthread_cond_wait(&condition, &mutex)需要传条件变量与一个互斥体结构,而且pthread_cond_wait之前必须获得互斥锁。其中原因简单来说就是条件变量本身也是需要加锁保护的资源。具体解释可以参考:http://stackoverflow.com/questions/6312342/pthread-cond-wait-and-mutex-requirement】

7.2.2 线程池原理

来看看threadpool.c 文件的几个相关函数:

图7-2-1.png<center>图7-2-1</center>

这里有四个环节:

1 创建工作线程:

这里的init_once函数调用uv_thread_create创建了nthreads数量的工作线程,nthread默认为4。worker为工作线程的执行函数。

看到图7-1-2,有一行uv_once(&once, init_once); 【uv_once对应的系统调用是pthread_once】。该行代码保证了init_once 有且仅被执行一次。在第一次调用uv__work_submit()时会执行一次init_once()。

2 工作线程进入等待:

看到worker线程最终会陷入uv_cond_wait【对应的系统调用是pthread_cond_wait】进行等待,且idle_threads自增。

这里的&cond、&mutex分别是一个全局的静态条件变量、互斥体。

3 提交任务到线程池:

看到post函数通过uv_cond_signal【对应的系统调用是pthread_cond_signal】向相应的条件变量——cond发送信号,处在uv_cond_wait挂起等待的工作线程当中的某个被激活。

worker线程往下执行,从wq取出w(保存的过程见7.1节),执行w->work()(对应7.1节中的uv_fs_work)。

4 通知主线程的事件循环:

工作线程完成任务后,调用uv_async_send通知主线程某个统一的io观察者。这里的机制7.3节讲。

7.3 线程池统一的io观察者 —— 管道、事件对象

7.3.1 管道、事件对象

管道、事件对象都是系统提供的机制,都可以用于线程间发送数据,所以这里可以用于线程间的通知。

1 管道

管道的相关系统调用是pipe()、pipe2()。参考 http://man7.org/linux/man-pages/man2/pipe.2.html

管道会创建两个fd,往fd[1]写数据,那么fd[0]就会收到数据。那么只需要把fd[0]添加到epoll_wait()所监听的io观察者队列里面,在工作线程需要通知的时候往fd[1]写数据,即能在主线程的epoll里面监听其他工作线程任务完成的通知。

2 事件对象

事件对象的相关系统调用是eventfd()、eventfd2()。参考 http://man7.org/linux/man-pages/man2/eventfd.2.html

与管道不同的是eventfd()只会创建一个fd,事件对象的读写都通过这个fd。事件对象内部维护一个counter,往fd写一个8字节的整数,会往counter加,而读的时候会返回counter,如果counter为0,那么读操作会阻塞住(fd为阻塞模式)。而这个fd也是可以交由epoll机制进行监听的,那么也可以达到使用管道一样的目的。

3 使用哪个?

这里libuv创建异步io观察者fd时,优先使用eventfd,如果系统不支持事件对象,就使用管道替代。看一下相关实现:

7-3-1.png<center>图7-3-1</center>

可以看到使用uv__eventfd2返回-1(errno = ENOSYS)时,uv__async_start里面使用管道替代了事件对象。而判断系统是否支持eventfd,是通过__NR_eventfd2宏去判断。

<font color=“red”>这里需要注意的是:使用宏进行判断__NR_eventfd是否defined是在编译期,而uv__async_start的执行是在运行期,也就是说,如果你在不支持事件对象的系统编译之后,在支持事件对象的系统上运行,那么uv__eventfd2始终是返回-1的。</font>

7.3.2 异步io观察者
7.3.2.1 数据结构 —— struct uv__async

在6.1.3节讲了持有io观察者的结构体 uv_tcp_s,6.2.4节讲了网络io操作如何封装成uv_tcp_t结构体、并构造对应的io_watcher,6.3.1和6.4节讲了如何把io_watcher加进uv_loop_t default_loop_struct的watcher_queue队列里。

那么类似于网络io操作的io观察者(uv__io_t io_watcher)由uv_tcp_s结构体来持有,这里要讨论的异步io观察者也是由一个数据结构(struct uv__async)持有的io观察者。通过把持有的io观察者(io_watcher)加进loop->watcher_queue队列,来加进到epoll的观察者队列中。

看到6.1.1节中关于struct uv_loop_s default_loop_struct的截图,发现uv_loop_s里面有个成员 struct uv__async async_watcher。这个就是管理统一异步io观察者的数据结构,一个事件循环结构体(uv_loop_t)有且只有一个。类似于uv_tcp_s。

看一些uv__async的定义,也持有一个uv__io_t io_watcher,还有封装了一个cb:

7-3-2.png<center>图7-3-2</center>

7.3.2.2 异步io观察者的保存与回调

我们知道一个uv_tcp_t的io观察者,是在用户调用了网络io之后,才加进到loop->watcher_queue里面的。那么这个异步io观察者是在node启动时,通过一连串调用node::Start() -> uv_default_loop() -> uv_loop_init() -> uv_async_init() -> uv__async_start(),最终调用uv__io_start(),把loop->async_watcher所持有的io_watcher加进loop->watcher_queue的。uv__async_start()也是创建事件对象/管道的地方,在上图的7-3-1可以看到。

来看一下loop->async_watcher和loop->async_watcher.io_watcher封装的回调函数。

7-3-3.png<center>图7-3-3</center>

可以看到loop->async_watcher.io_watcher->cb 是uv__async_io;

loop->async_watcher.cb 是uv__async_event。

7.2.2节讲到worker线程完成w->work()之后,通过uv_async_send通知异步io观察者,uv_async_send的操作就是往事件对象/管道写东西,那么当io观察者收到数据,uv_run()里面的epoll_wait()返回该io_watcher的fd时,uv__async_io会先被回调,在uv__async_io里面会进而调用uv__async_event。看下代码:

7-3-4.png<center>图7-3-4</center>

uv__aysnc_io里面取出的wa就是loop->async_watcher,所以wa->cb就是uv__async_event。

7.4 线程池异步io之后的回调工作

讲到uv__async_event这一步,我们回想一下此时应该执行什么处理:worker线程执行完了w->work()(其中w是提交线程池的请求结构体 uv__work),然后通知事件循环需要在主线程执行w->done(),而通知的这个过程就是通过 uv_async_send()往管道/事件对象写数据,激活epoll_wait(),根据返回的fd,由loop->watchers映射表拿到异步io观察者 —— loop->async_watcher.io_watcher,然后层层回调到uv__async_event,那么这个时候,我们是否要调用线程池完成了w->work()之后剩余的w->done()?

7.4.1 uv__async_event() 到 uv__work_done()

node里面多次使用void*[2]类型来维护一个链表,loop->async_handles也是。可以看到图6-1-1。那么async_handles保存什么链表呢?

看到图7-3-4,uv__async_event()就是从loop->async_handles链表里,取出struct uv_async_t结构类型的元素h,并调用回调函数h->async_cb()。

再看到图7-3-3,uv_async_init()里面,往loop->async_handles里面添加了struct uv_async_t* t。7.3.2.2节讲到的一系列调用流程有:uv_loop_init() -> uv_async_init(),看下uv_loop_init()调用uv_async_init()的代码:

7-4-1.png<center>图7-4-1</center>

可以看到uv_loop_init()传给uv_async_init()的uv_async_t 是loop->wq_async,而async_cb是uv__work_done。

所以最终异步io观察者被激活之后,主线程回调到了uv__work_done()。uv__work_done在线程池模块(deps/uv/src/threadpool.c)里面。

7.4.2 uv__work_done()

看一下uv__work_done()的代码:

7-4-2.png<center>图7-4-2</center>

在7.1.2节就讲了post()提交请求时,往全局队列wq添加一个uv__work数据结构,那么最终uv__work_done()被调用的时候,从该wq取出所有w,执行w->done(),完成最终的回调。这里的w->done()就是7.1节中提到的fs__work_done()。

注意了,这里的uv__work_done()是在主线程执行的,也就是你的js代码由始至终在同一个线程里面执行。

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

    本文给出了一个通用的线程池框架,该框架将与线程执行相关的任务进行了高层次的抽象,使之与具体的执行任务无关。另外该线程池具有动态伸缩性,它能根据执行任务的轻重自动调整线程池中线程的数量。文章的最后,我们给出一个简单示例程序,通过该示例程序,我们会发现,通过该线程池框架执行多线程任务是多么的简单。


1. 为什么需要线程池

  目前的大多数网络服务器,包括Web服务器、Email服务器以及数据库服务器等都具有一个共同点,就是单位时间内必须处理数目巨大的连接请求,但处理时间却相对较短。
  传统多线程方案中我们采用的服务器模型则是一旦接受到请求之后,即创建一个新的线程,由该线程执行任务。任务执行完毕后,线程退出,这就是是“即时创建,即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于不停的创建线程,销毁线程的状态。
  我们将传统方案中的线程执行过程分为三个过程:T1、T2、T3。
  T1:线程创建时间
  T2:线程执行时间,包括线程的同步等时间
  T3:线程销毁时间
  那么我们可以看出,线程本身的开销所占的比例为(T1+T3) / (T1+T2+T3)。如果线程执行的时间很短的话,这比开销可能占到20%-50%左右。如果任务执行时间很频繁的话,这笔开销将是不可忽略的。
  除此之外,线程池能够减少创建的线程个数。通常线程池所允许的并发线程是有上界的,如果同时需要并发的线程数超过上界,那么一部分线程将会等待。而传统方案中,如果同时请求数目为2000,那么最坏情况下,系统可能需要产生2000个线程。尽管这不是一个很大的数目,但是也有部分机器可能达不到这种要求。
  因此线程池的出现正是着眼于减少线程池本身带来的开销。线程池采用预创建的技术,在应用程序启动之后,将立即创建一定数量的线程(N1),放入空闲队列中。这些线程都是处于阻塞(Suspended)状态,不消耗CPU,但占用较小的内存空间。当任务到来后,缓冲池选择一个空闲线程,把任务传入此线程中运行。当N1个线程都在处理任务后,缓冲池自动创建一定数量的新线程,用于处理更多的任务。在任务执行完毕后线程也不退出,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程都一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。
  基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小,不过我们另外可能需要考虑进去线程之间同步所带来的开销。

2. 构建线程池框架

  一般线程池都必须具备下面几个组成部分:
  线程池管理器:用于创建并管理线程池
  工作线程:线程池中实际执行的线程
  任务接口:尽管线程池大多数情况下是用来支持网络服务器,但是我们将线程执行的任务抽象出来,形成任务接口,从而是的线程池与具体的任务无关。
  任务队列:线程池的概念具体到实现则可能是队列,链表之类的数据结构,其中保存执行线程。
  我们实现的通用线程池框架由五个重要部分组成CThreadManage,CThreadPool,CThread,CJob,CWorkerThread,除此之外框架中还包括线程同步使用的类CThreadMutex和CCondition。
  CJob是所有的任务的基类,其提供一个接口Run,所有的任务类都必须从该类继承,同时实现Run方法。该方法中实现具体的任务逻辑。
  CThread是Linux中线程的包装,其封装了Linux线程最经常使用的属性和方法,它也是一个抽象类,是所有线程类的基类,具有一个接口Run。
  CWorkerThread是实际被调度和执行的线程类,其从CThread继承而来,实现了CThread中的Run方法。
  CThreadPool是线程池类,其负责保存线程,释放线程以及调度线程。
  CThreadManage是线程池与用户的直接接口,其屏蔽了内部的具体实现。
  CThreadMutex用于线程之间的互斥。
  CCondition则是条件变量的封装,用于线程之间的同步。
  它们的类的继承关系如下图所示: (TO ADD) 
  线程池的时序很简单,如下图所示:(TO ADD)。
  CThreadManage直接跟客户端打交道,其接受需要创建的线程初始个数,并接受客户端提交的任务。这儿的任务是具体的非抽象的任务。CThreadManage的内部实际上调用的都是CThreadPool的相关操作。CThreadPool创建具体的线程,并把客户端提交的任务分发给CWorkerThread,CWorkerThread实际执行具体的任务。

3. 理解系统组件

  下面我们分开来了解系统中的各个组件。
  CThreadManage
  CThreadManage的功能非常简单,其提供最简单的方法,其类定义如下:


class CThreadManage {
private:
    CThreadPool* m_Pool;
    int m_NumOfThread;
public:
    CThreadManage();
    CThreadManage(int num);
    virtual ~CThreadManage();

    void SetParallelNum(int num); 
    void Run(CJob* job,void* jobdata);
    void TerminateAll(void);
};

  其中m_Pool指向实际的线程池;m_NumOfThread是初始创建时候允许创建的并发的线程个数。另外Run和TerminateAll方法也非常简单,只是简单的调用CThreadPool的一些相关方法而已。其具体的实现如下:

复制代码
CThreadManage::CThreadManage() {
    m_NumOfThread = 10;
    m_Pool = new CThreadPool(m_NumOfThread);
}

CThreadManage::CThreadManage(int num) {
    m_NumOfThread = num;
    m_Pool = new CThreadPool(m_NumOfThread);
}

CThreadManage::~CThreadManage() {
    if(NULL != m_Pool)
        delete m_Pool;
}

void CThreadManage::SetParallelNum(int num) {
    m_NumOfThread = num;
}

void CThreadManage::Run(CJob* job,void* jobdata) {
    m_Pool->Run(job,jobdata);
}

void CThreadManage::TerminateAll(void) {
    m_Pool->TerminateAll();
}

  CThread

  CThread 类实现了对Linux中线程操作的封装,它是所有线程的基类,也是一个抽象类,提供了一个抽象接口Run,所有的CThread都必须实现该Run方法。CThread的定义如下所示:


class CThread {
private:
    int m_ErrCode;
    Semaphore m_ThreadSemaphore; //the inner semaphore, which is used to realize
    unsigned long m_ThreadID; 
    bool m_Detach; //The thread is detached
    bool m_CreateSuspended; //if suspend after creating
    char* m_ThreadName;
    ThreadState m_ThreadState; //the state of the thread
protected:
    void SetErrcode(int errcode){m_ErrCode = errcode;}
    static void* ThreadFunction(void*);
public:
    CThread();
    CThread(bool createsuspended,bool detach);
    virtual ~CThread();

    virtual void Run(void) = 0;
    void SetThreadState(ThreadState state){m_ThreadState = state;}
    bool Terminate(void); //Terminate the threa
    bool Start(void); //Start to execute the thread
    void Exit(void);
    bool Wakeup(void);
    ThreadState GetThreadState(void){return m_ThreadState;}
    int GetLastError(void){return m_ErrCode;}
    void SetThreadName(char* thrname){strcpy(m_ThreadName,thrname);}
    char* GetThreadName(void){return m_ThreadName;}
    int GetThreadID(void){return m_ThreadID;}
    bool SetPriority(int priority);
    int GetPriority(void);
    int GetConcurrency(void);
    void SetConcurrency(int num);
    bool Detach(void);
    bool Join(void);
    bool Yield(void);
    int Self(void);
};

  线程的状态可以分为四种,空闲、忙碌、挂起、终止(包括正常退出和非正常退出)。由于目前Linux线程库不支持挂起操作,因此,我们的此处的挂起操作类似于暂停。如果线程创建后不想立即执行任务,那么我们可以将其“暂停”,如果需要运行,则唤醒。有一点必须注意的是,一旦线程开始执行任务,将不能被挂起,其将一直执行任务至完毕。
  线程类的相关操作均十分简单。线程的执行入口是从Start()函数开始,其将调用函数ThreadFunction,ThreadFunction再调用实际的Run函数,执行实际的任务。

  CThreadPool

  CThreadPool是线程的承载容器,一般可以将其实现为堆栈、单向队列或者双向队列。在我们的系统中我们使用STL Vector对线程进行保存。CThreadPool的实现代码如下:


class CThreadPool {
    friend class CWorkerThread;
private:
    unsigned int m_MaxNum; //the max thread num that can create at the same time
    unsigned int m_AvailLow; //The min num of idle thread that shoule kept
    unsigned int m_AvailHigh; //The max num of idle thread that kept at the same time
    unsigned int m_AvailNum; //the normal thread num of idle num;
    unsigned int m_InitNum; //Normal thread num;
protected:
    CWorkerThread* GetIdleThread(void); 
    void AppendToIdleList(CWorkerThread* jobthread);
    void MoveToBusyList(CWorkerThread* idlethread);
    void MoveToIdleList(CWorkerThread* busythread);
    void DeleteIdleThread(int num);
    void CreateIdleThread(int num);
public:
    CThreadMutex m_BusyMutex; //when visit busy list,use m_BusyMutex to lock and unlock
    CThreadMutex m_IdleMutex; //when visit idle list,use m_IdleMutex to lock and unlock
    CThreadMutex m_JobMutex; //when visit job list,use m_JobMutex to lock and unlock
    CThreadMutex m_VarMutex;
    CCondition m_BusyCond; //m_BusyCond is used to sync busy thread list
    CCondition m_IdleCond; //m_IdleCond is used to sync idle thread list
    CCondition m_IdleJobCond; //m_JobCond is used to sync job list
    CCondition m_MaxNumCond;
    vector<CWorkerThread*> m_ThreadList;
    vector<CWorkerThread*> m_BusyList; //Thread List
    vector<CWorkerThread*> m_IdleList; //Idle List
    CThreadPool();
    CThreadPool(int initnum);
    virtual ~CThreadPool(); 
    void SetMaxNum(int maxnum){m_MaxNum = maxnum;}
    int GetMaxNum(void){return m_MaxNum;}
    void SetAvailLowNum(int minnum){m_AvailLow = minnum;}
    int GetAvailLowNum(void){return m_AvailLow;}
    void SetAvailHighNum(int highnum){m_AvailHigh = highnum;}
    int GetAvailHighNum(void){return m_AvailHigh;}
    int GetActualAvailNum(void){return m_AvailNum;}
    int GetAllNum(void){return m_ThreadList.size();}
    int GetBusyNum(void){return m_BusyList.size();}
    void SetInitNum(int initnum){m_InitNum = initnum;}
    int GetInitNum(void){return m_InitNum;}
    void TerminateAll(void);
    void Run(CJob* job,void* jobdata);
};

CThreadPool::CThreadPool() {
    m_MaxNum = 50;
    m_AvailLow = 5;
    m_InitNum=m_AvailNum = 10 ; 
    m_AvailHigh = 20;
    m_BusyList.clear();
    m_IdleList.clear();
    for(int i=0;i<m_InitNum;i++) {
        CWorkerThread* thr = new CWorkerThread();
        thr->SetThreadPool(this);
        AppendToIdleList(thr);
        thr->Start();
    }
}

CThreadPool::CThreadPool(int initnum) {
    assert(initnum>0 && initnum<=30);
    m_MaxNum = 30;
    m_AvailLow = initnum-10>0?initnum-10:3;
    m_InitNum=m_AvailNum = initnum ; 
    m_AvailHigh = initnum+10;
    m_BusyList.clear();
    m_IdleList.clear();
    for(int i=0;i<m_InitNum;i++) {
        CWorkerThread* thr = new CWorkerThread();
        AppendToIdleList(thr);
        thr->SetThreadPool(this);
        thr->Start(); //begin the thread,the thread wait for job
    }
}

CThreadPool::~CThreadPool() {
    TerminateAll();
}

void CThreadPool::TerminateAll() {
    for(int i=0;i < m_ThreadList.size();i++) {
        CWorkerThread* thr = m_ThreadList[i];
        thr->Join();
    }
    return;
}

CWorkerThread* CThreadPool::GetIdleThread(void) {
    while(m_IdleList.size() ==0 )
    m_IdleCond.Wait();
    m_IdleMutex.Lock();
    if(m_IdleList.size() > 0) {
        CWorkerThread* thr = (CWorkerThread*)m_IdleList.front();
        printf("Get Idle thread %d/n",thr->GetThreadID());
        m_IdleMutex.Unlock();
        return thr;
    }
    m_IdleMutex.Unlock();
    return NULL;
}

//add an idle thread to idle list
void CThreadPool::AppendToIdleList(CWorkerThread* jobthread) {
    m_IdleMutex.Lock();
    m_IdleList.push_back(jobthread);
    m_ThreadList.push_back(jobthread);
    m_IdleMutex.Unlock();
}

//move and idle thread to busy thread
void CThreadPool::MoveToBusyList(CWorkerThread* idlethread) {
    m_BusyMutex.Lock();
    m_BusyList.push_back(idlethread);
    m_AvailNum--;
    m_BusyMutex.Unlock();
    
    m_IdleMutex.Lock();
    vector<CWorkerThread*>::iterator pos;
    pos = find(m_IdleList.begin(),m_IdleList.end(),idlethread);
    if(pos !=m_IdleList.end())
        m_IdleList.erase(pos);
    m_IdleMutex.Unlock();
}

void CThreadPool::MoveToIdleList(CWorkerThread* busythread) {
    m_IdleMutex.Lock();
    m_IdleList.push_back(busythread);
    m_AvailNum++;
    m_IdleMutex.Unlock();
    m_BusyMutex.Lock();
    vector<CWorkerThread*>::iterator pos;
    pos = find(m_BusyList.begin(),m_BusyList.end(),busythread);
    if(pos!=m_BusyList.end())
        m_BusyList.erase(pos);
    m_BusyMutex.Unlock();
    m_IdleCond.Signal();
    m_MaxNumCond.Signal();
}

//create num idle thread and put them to idlelist
void CThreadPool::CreateIdleThread(int num) {
    for(int i=0;i<num;i++) {
        CWorkerThread* thr = new CWorkerThread();
        thr->SetThreadPool(this);
        AppendToIdleList(thr);
        m_VarMutex.Lock();
        m_AvailNum++;
        m_VarMutex.Unlock();
        thr->Start(); //begin the thread,the thread wait for job
    }
}

void CThreadPool::DeleteIdleThread(int num)
{
    printf("Enter into CThreadPool::DeleteIdleThread/n");
    m_IdleMutex.Lock();
    printf("Delete Num is %d/n",num);
    for(int i=0;i<num;i++){
        CWorkerThread* thr;
        if(m_IdleList.size() > 0 ){
            thr = (CWorkerThread*)m_IdleList.front();
            printf("Get Idle thread %d/n",thr->GetThreadID());
        }

        vector<CWorkerThread*>::iterator pos;
        pos = find(m_IdleList.begin(),m_IdleList.end(),thr);
        if(pos!=m_IdleList.end())
        m_IdleList.erase(pos);
        m_AvailNum--;
        printf("The idle thread available num:%d /n",m_AvailNum);
        printf("The idlelist num:%d /n",m_IdleList.size());
    }
    m_IdleMutex.Unlock();
}

void CThreadPool::Run(CJob* job,void* jobdata) {
    assert(job!=NULL);

    //if the busy thread num adds to m_MaxNum,so we should wait
    if(GetBusyNum() == m_MaxNum)
        m_MaxNumCond.Wait();

    if(m_IdleList.size()<m_AvailLow) {
        if(GetAllNum()+m_InitNum-m_IdleList.size() < m_MaxNum)
            CreateIdleThread(m_InitNum-m_IdleList.size());
        else
            CreateIdleThread(m_MaxNum-GetAllNum());
    }

    CWorkerThread* idlethr = GetIdleThread();
    if(idlethr !=NULL) {
        idlethr->m_WorkMutex.Lock();
        MoveToBusyList(idlethr);
        idlethr->SetThreadPool(this);
        job->SetWorkThread(idlethr);
        printf("Job is set to thread %d /n",idlethr->GetThreadID());
        idlethr->SetJob(job,jobdata);
    }
}

  在CThreadPool中存在两个链表,一个是空闲链表,一个是忙碌链表。Idle链表中存放所有的空闲进程,当线程执行任务时候,其状态变为忙碌状态,同时从空闲链表中删除,并移至忙碌链表中。在CThreadPool的构造函数中,我们将执行下面的代码:

for(int i=0;i<m_InitNum;i++) {
    CWorkerThread* thr = new CWorkerThread();
    AppendToIdleList(thr);
    thr->SetThreadPool(this);
    thr->Start(); //begin the thread,the thread wait for job
}

  在该代码中,我们将创建m_InitNum个线程,创建之后即调用AppendToIdleList放入Idle链表中,由于目前没有任务分发给这些线程,因此线程执行Start后将自己挂起。
  事实上,线程池中容纳的线程数目并不是一成不变的,其会根据执行负载进行自动伸缩。为此在CThreadPool中设定四个变量:
  m_InitNum:处世创建时线程池中的线程的个数。
  m_MaxNum:当前线程池中所允许并发存在的线程的最大数目。
  m_AvailLow:当前线程池中所允许存在的空闲线程的最小数目,如果空闲数目低于该值,表明负载可能过重,此时有必要增加空闲线程池的数目。实现中我们总是将线程调整为m_InitNum个。
  m_AvailHigh:当前线程池中所允许的空闲的线程的最大数目,如果空闲数目高于该值,表明当前负载可能较轻,此时将删除多余的空闲线程,删除后调整数也为m_InitNum个。
  m_AvailNum:目前线程池中实际存在的线程的个数,其值介于m_AvailHigh和m_AvailLow之间。如果线程的个数始终维持在m_AvailLow和m_AvailHigh之间,则线程既不需要创建,也不需要删除,保持平衡状态。因此如何设定m_AvailLow和m_AvailHigh的值,使得线程池最大可能的保持平衡态,是线程池设计必须考虑的问题。
  线程池在接受到新的任务之后,线程池首先要检查是否有足够的空闲池可用。检查分为三个步骤:
  (1)检查当前处于忙碌状态的线程是否达到了设定的最大值m_MaxNum,如果达到了,表明目前没有空闲线程可用,而且也不能创建新的线程,因此必须等待直到有线程执行完毕返回到空闲队列中。
  (2)如果当前的空闲线程数目小于我们设定的最小的空闲数目m_AvailLow,则我们必须创建新的线程,默认情况下,创建后的线程数目应该为m_InitNum,因此创建的线程数目应该为( 当前空闲线程数与m_InitNum);但是有一种特殊情况必须考虑,就是现有的线程总数加上创建后的线程数可能超过m_MaxNum,因此我们必须对线程的创建区别对待。

if(GetAllNum()+m_InitNum-m_IdleList.size() < m_MaxNum)
    CreateIdleThread(m_InitNum-m_IdleList.size());
else
    CreateIdleThread(m_MaxNum-GetAllNum());

  如果创建后总数不超过m_MaxNum,则创建后的线程为m_InitNum;如果超过了,则只创建( m_MaxNum-当前线程总数 )个。
  (3)调用GetIdleThread方法查找空闲线程。如果当前没有空闲线程,则挂起;否则将任务指派给该线程,同时将其移入忙碌队列。
当线程执行完毕后,其会调用MoveToIdleList方法移入空闲链表中,其中还调用m_IdleCond.Signal()方法,唤醒GetIdleThread()中可能阻塞的线程。
  CJob
  CJob类相对简单,其封装了任务的基本的属性和方法,其中最重要的是Run方法,代码如下:


class CJob {
private:
    int m_JobNo; //The num was assigned to the job
    char* m_JobName; //The job name
    CThread *m_pWorkThread; //The thread associated with the job
public:
    CJob( void );
    virtual ~CJob();

    int GetJobNo(void) const { return m_JobNo; }
    void SetJobNo(int jobno){ m_JobNo = jobno;}
    char* GetJobName(void) const { return m_JobName; }
    void SetJobName(char* jobname);
    CThread *GetWorkThread(void){ return m_pWorkThread; }
    void SetWorkThread ( CThread *pWorkThread ){
        m_pWorkThread = pWorkThread;
    }
    virtual void Run ( void *ptr ) = 0;
};

CJob::CJob(void):m_pWorkThread(NULL),m_JobNo(0),m_JobName(NULL){}

CJob::~CJob(){
    if(NULL != m_JobName)
        free(m_JobName);
}

void CJob::SetJobName(char* jobname) {
    if(NULL !=m_JobName) {
        free(m_JobName);
        m_JobName = NULL;
    }
    if(NULL !=jobname) {
        m_JobName = (char*)malloc(strlen(jobname)+1);
        strcpy(m_JobName,jobname);
    }
}

4. 线程池使用示例

  至此我们给出了一个简单的与具体任务无关的线程池框架。使用该框架非常的简单,我们所需要的做的就是派生CJob类,将需要完成的任务实现在Run方法中。然后将该Job交由CThreadManage去执行。下面我们给出一个简单的示例程序:

复制代码
class CXJob: public CJob {
public:
    CXJob(){i=0;}
    ~CXJob(){}
    void Run(void* jobdata) {
        printf("The Job comes from CXJOB/n");
        sleep(2);
    }
};

class CYJob: public CJob {
public:
    CYJob(){i=0;}
    ~CYJob(){}
    void Run(void* jobdata) {
        printf("The Job comes from CYJob/n");
    }
};

void main() {
    CThreadManage* manage = new CThreadManage(10);
    for(int i=0;i<40;i++) {
        CXJob* job = new CXJob();
        manage->Run(job,NULL);
    }
    sleep(2);
    CYJob* job = new CYJob();
    manage->Run(job,NULL);
    manage->TerminateAll();
}

  CXJob和CYJob都是从Job类继承而来,其都实现了Run接口。CXJob只是简单的打印一句”The Job comes from CXJob”,CYJob也只打印”The Job comes from CYJob”,然后均休眠2秒钟。在主程序中我们初始创建10个工作线程。然后分别执行40次CXJob和一次CYJob。


5. 线程池使用后记

线程池适合场合
  事实上,线程池并不是万能的。它有其特定的使用场合。线程池致力于减少线程本身的开销对应用所产生的影响,这是有前提的,前提就是线程本身开销与线程执行任务相比不可忽略。如果线程本身的开销相对于线程任务执行开销而言是可以忽略不计的,那么此时线程池所带来的好处是不明显的,比如对于FTP服务器以及Telnet服务器,通常传送文件的时间较长,开销较大,那么此时,我们采用线程池未必是理想的方法,我们可以选择“即时创建,即时销毁”的策略。
  总之线程池通常适合下面的几个场合:
  (1) 单位时间内处理任务频繁而且任务处理时间短
  (2) 对实时性要求较高。如果接受到任务后在创建线程,可能满足不了实时要求,因此必须采用线程池进行预创建。
  (3) 必须经常面对高突发性事件,比如Web服务器,如果有足球转播,则服务器将产生巨大的冲击。此时如果采取传统方法,则必须不停的大量产生线程,销毁线程。此时采用动态线程池可以避免这种情况的发生。

 

整理自http://blog.chinaunix.net/uid-25073805-id-3046000.html


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值