Linux多线程实践(9) --简单线程池的设计与实现
线程池的技术背景
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。如何利用已有对象来服务(不止一个不同的任务)就是一个需要解决的关键问题,其实这就是一些"池化资源"技术产生的原因。比如大家所熟悉的数据库连接池正是遵循这一思想而产生的,本文将介绍的线程池技术同样符合这一思想。
目前,一些著名的大公司都特别看好这项技术,并早已经在他们的产品中应用该技术。比如IBM的WebSphere,IONA的Orbix 2000在SUN的 Jini中,Microsoft的MTS(Microsoft Transaction Server 2.0),COM+等。
现在您是否也想在服务器程序应用该项技术?
线程池技术如何提高服务器程序的性能
我所提到服务器程序是指能够接受客户请求并能处理请求的程序,而不只是指那些接受网络客户请求的网络服务器程序。
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。但如果对多线程应用不当,会增加对单个任务的处理时间。可以举一个简单的例子:
假设在一台服务器完成一项任务的时间为T
T1 创建线程的时间
T2 在线程中执行任务的时间,包括线程间同步所需时间
T3 线程销毁的时间
显然T = T1+T2+T3。注意这是一个极度简化的假设。
可以看出T1,T3是多线程本身的带来的开销,我们渴望减少T1,T3所用的时间,从而减少T的时间。但一些线程的使用者并没有注意到这一点,所以在程序中频繁的创建或销毁线程,这导致T1和T3在T中占有相当比例(在传统的多线程服务器模型中是这样实现的:一旦有个请求到达,就创建一个新的线程,由该线程执行任务,任务执行完毕之后,线程就退出。这就是"即时创建,即时销毁"的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数非常频繁,那么服务器就将处于一个不停的创建线程和销毁线程的状态。这笔开销是不可忽略的,尤其是线程执行的时间非常非常短的情况。)。显然这是突出了线程的弱点(T1,T3),而不是优点(并发性)。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段(在应用程序启动之后,就马上创建一定数量的线程,放入空闲的队列中。这些线程都是处于阻塞状态,这些线程只占一点内存,不占用CPU。当任务到来后,线程池将选择一个空闲的线程,将任务传入此线程中运行。当所有的线程都处在处理任务的时候,线程池将自动创建一定的数量的新线程,用于处理更多的任务。执行任务完成之后线程并不退出,而是继续在线程池中等待下一次任务。当大部分线程处于阻塞状态时,线程池将自动销毁一部分的线程,回收系统资源),这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目。再看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。我们比较利用线程池技术和不利于线程池技术的服务器处理这些请求时所产生的线程总数。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目或者上限(以下简称线程池尺寸),而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池尺寸是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。
简单线程池的实现
下面是一个简单线程池的实现, 它所使用的方案如下:
1.程序启动之前,初始化线程池,此时线程池中没有任何线程, 需要调用addTask方法向线程池中添加任务;
2.如果此时线程池有空闲(处于等待)的线程, 就不会创建新的线程, 这样就省去了T1, T3的时间;
3.如果此时线程池中没有处于等待的线程(由于此时线程刚刚初始化, 此时线程池中肯定是没有处于等待状态的线程的)并且此时线程池中的线程数并没有达到阈值, 才创建并启动线程;
4.如果此时线程池中的线程数已经达到阈值, 那就只能等待现在还执行任务的线程, 等到其执行完其当前正在执行任务, 然后才从任务队列中将新任务取出然后执行;
线程池主要由两个文件组成, 一个threadpool.h头文件和一个threadpool.cpp源文件组成。源码中已有重要的注释,就不加以分析了。
//ThreadPool设计 void *thread_routine(void *args); class ThreadPool { friend void *thread_routine(void *args); private: //回调函数类型 typedef void *(*callback_t)(void *); //任务结构体 struct task_t { callback_t run; //任务回调函数 void *args; //任务函数参数 }; public: ThreadPool(int _maxThreads = 36, unsigned int _waitSeconds = 2); ~ThreadPool(); //添加任务接口 void addTask(callback_t run, void *args); private: void startTask(); private: Condition ready; //任务准备就绪或线程池销毁通知 std::queue<task_t *> taskQueue; //任务队列 unsigned int maxThreads; //线程池最多允许的线程数 unsigned int counter; //线程池当前线程数 unsigned int idle; //线程池空闲线程数 unsigned int waitSeconds; //线程可以等待的秒数 bool quit; //线程池销毁标志 };
//构造函数 ThreadPool::ThreadPool(int _maxThreads, unsigned int _waitSeconds) : maxThreads(_maxThreads), counter(0), idle(0), waitSeconds(_waitSeconds), quit(false) {}
// 线程入口函数 // 这其实就相当于一个消费者线程, 不断的消费任务(执行任务) void *thread_routine(void *args) { //将子线程设置成为分离状态, 这样主线程就可以不用jion pthread_detach(pthread_self()); printf("*thread 0x%lx is starting...\n", (unsigned long)pthread_self()); ThreadPool *pool = (ThreadPool *)args; //等待任务的到来, 然后执行任务 while (true) { bool timeout = false; pool->ready.lock(); //当处于等待的时候, 则说明空闲的线程多了一个 ++ pool->idle; //pool->ready中的条件变量有三个作用: // 1.等待任务队列中有任务到来 // 2.等待线程池销毁通知 // 3.确保当等待超时的时候, 能够将线程销毁(线程退出) while (pool->taskQueue.empty() && pool->quit == false) { printf("thread 0x%lx is waiting...\n", (unsigned long)pthread_self()); //等待waitSeconds if (0 != pool->ready.timedwait(pool->waitSeconds)) { //如果等待超时 printf("thread 0x%lx is wait timeout ...\n", (unsigned long)pthread_self()); timeout = true; //break出循环, 继续向下执行, 会执行到下面第1个if处 break; } } //条件成熟(当等待结束), 线程开始执行任务或者是线程销毁, 则说明空闲线程又少了一个 -- pool->idle; // 状态3.如果等待超时(一般此时任务队列已经空了) if (timeout == true && pool->taskQueue.empty()) { -- pool->counter; //解锁然后跳出循环, 直接销毁线程(退出线程) pool->ready.unlock(); break; } // 状态2.如果是等待到了线程的销毁通知, 且任务都执行完毕了 if (pool->quit == true && pool->taskQueue.empty()) { -- pool->counter; //如果没有线程了, 则给线程池发送通知 //告诉线程池, 池中已经没有线程了 if (pool->counter == 0) pool->ready.signal(); //解锁然后跳出循环 pool->ready.unlock(); break; } // 状态1.如果是有任务了, 则执行任务 if (!(pool->taskQueue.empty())) { //从队头取出任务进行处理 ThreadPool::task_t *t = pool->taskQueue.front(); pool->taskQueue.pop(); //执行任务需要一定的时间 //解锁以便于其他的生产者可以继续生产任务, 其他的消费者也可以消费任务 pool->ready.unlock(); //处理任务 t->run(t->args); delete t; } } //跳出循环之后, 打印退出信息, 然后销毁线程 printf("thread 0x%lx is exiting...\n", (unsigned long)pthread_self()); pthread_exit(NULL); }
//addTask函数 //添加任务函数, 类似于一个生产者, 不断的将任务生成, 挂接到任务队列上, 等待消费者线程进行消费 void ThreadPool::addTask(callback_t run, void *args) { /** 1. 生成任务并将任务添加到"任务队列"队尾 **/ task_t *newTask = new task_t {run, args}; ready.lock(); //注意需要使用互斥量保护共享变量 taskQueue.push(newTask); /** 2. 让线程开始执行任务 **/ startTask(); ready.unlock();//解锁以使任务开始执行 }
//线程启动函数 void ThreadPool::startTask() { // 如果有等待线程, 则唤醒其中一个, 让它来执行任务 if (idle > 0) ready.signal(); // 没有等待线程, 而且当前先线程总数尚未达到阈值, 我们就需要创建一个新的线程 else if (counter < maxThreads) { pthread_t tid; pthread_create(&tid, NULL, thread_routine, this); ++ counter; } }
//析构函数 ThreadPool::~ThreadPool() { //如果已经调用过了, 则直接返回 if (quit == true) return; ready.lock(); quit = true; if (counter > 0) { //对于处于等待状态, 则给他们发送通知, //这些处于等待状态的线程, 则会接收到通知, //然后直接退出 if (idle > 0) ready.broadcast(); //对于正处于执行任务的线程, 他们接收不到这些通知, //则需要等待他们执行完任务 while (counter > 0) ready.wait(); } ready.unlock(); }
完整源代码:http://download.csdn.net/download/hanqing280441589/8449049
关于高级线程池的探讨
简单线程池存在一些问题,比如如果有大量的客户要求服务器为其服务,但由于线程池的工作线程是有限的,服务器只能为部分客户服务,其它客户提交的任务,只能在任务队列中等待处理。一些系统设计人员可能会不满这种状况,因为他们对服务器程序的响应时间要求比较严格,所以在系统设计时可能会怀疑线程池技术的可行性,但是线程池有相应的解决方案。调整优化线程池尺寸是高级线程池要解决的一个问题。主要有下列解决方案:
方案一:动态增加工作线程
在一些高级线程池中一般提供一个可以动态改变的工作线程数目的功能,以适应突发性的请求。一旦请求变少了将逐步减少线程池中工作线程的数目。当然线程增加可以采用一种超前方式,即批量增加一批工作线程,而不是来一个请求才建立创建一个线程。批量创建是更加有效的方式。该方案还有应该限制线程池中工作线程数目的上限和下限。否则这种灵活的方式也就变成一种错误的方式或者灾难,因为频繁的创建线程或者短时间内产生大量的线程将会背离使用线程池原始初衷--减少创建线程的次数。
举例:Jini中的TaskManager,就是一个精巧线程池管理器,它是动态增加工作线程的。SQL Server采用单进程(Single Process)多线程(Multi-Thread)的系统结构,1024个数量的线程池,动态线程分配,理论上限32767。
方案二:优化工作线程数目
如果不想在线程池应用复杂的策略来保证工作线程数满足应用的要求,你就要根据统计学的原理来统计客户的请求数目,比如高峰时段平均一秒钟内有多少任务要求处理,并根据系统的承受能力及客户的忍受能力来平衡估计一个合理的线程池尺寸。线程池的尺寸确实很难确定,所以有时干脆用经验值。
举例:在MTS中线程池的尺寸固定为100。
方案三:一个服务器提供多个线程池
在一些复杂的系统结构会采用这个方案。这样可以根据不同任务或者任务优先级来采用不同线程池处理。
举例:COM+用到了多个线程池。
这三种方案各有优缺点。在不同应用中可能采用不同的方案或者干脆组合这三种方案来解决实际问题。
线程池技术适用范围及应注意的问题
下面是我总结的一些线程池应用范围,可能是不全面的。
线程池的应用范围:
(1)需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
(2)对性能要求苛刻的应用,比如要求服务器迅速相应客户请求。
(3)接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并出现"OutOfMemory"的错误。
结束语
本文只是简单介绍线程池技术。可以看出线程池技术对于服务器程序的性能改善是显著的。线程池技术在服务器领域有着广泛的应用前景。希望这项技术能够应用到您的多线程服务程序中。
注:这是网上一篇博客的改造: 将Java版本的线程池改造成了基于Linux 的C++版本, 原文链接为:http://www.ibm.com/developerworks/cn/java/l-threadPool/, 如果读者的兴趣所在为Java, 请移步于此, 向您郑重推荐, 这是一篇非常好的文章, 谢谢!
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。