码迷,mamicode.com
首页 > 编程语言 > 详细

Linux下简单线程池的实现

时间:2016-03-09 16:10:42      阅读:252      评论:0      收藏:0      [点我收藏+]

标签:

线程池的技术背景

   在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在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、线程池,顾名思义,就是要创建很多线程。创建线程的函数pthread_creat()应该是最容易被想到的。有线程创建就要有线程退出pthread_exit(),在线程退出前,如果线程没有设置pthread_detach()属性,那么显然要回收线程资源pthread_join()。当然咯,可能要获取线程的ID值pthread_self()。
  2、第一步创建了线程,刚开始线程是不做事情的,初始化好了,就等待。等待当然不会是while(1)这种函数,因为那样太消耗CPU资源。容易想到的等待自然是使用条件变量的等待pthread_cond_wait(),这个函数干两件事情,第一件是对解除与形参mutex对应的互斥锁,然后是重新加锁,为的是在线程将任务放入任务队列的一个缓冲。任务放入完成后,再加锁,这样不会影响其他任务获取加锁的权利。因此,在调用该函数之前,会自然会想到加互斥锁。初始化互斥锁函数pthread_mutex_init(),反初始化互斥锁函数pthread_mutex_destroy(),加锁函数pthread_mutex_lock(),解锁函数pthread_mutex_unlock(),稍微再细化一点,可能会用到尝试解锁pthread_mutex_trylock()。
       3.、实现了上面二步,一个线程池的框架就初步搭起来了。当然没法用,因为真正干事情的线程全部在等待中,注意不应该是超时的等待pthread_cond_timewait()。要使处于阻塞状态的线程干事情,得用信号去唤醒它pthread_cond_signal(),“打鸟”的一个函数,开一枪,总会把这只鸟吵醒,但具体是那一只,看那只最先在那排队了(上面已经说了pthread_cond_wait()函数的等待队列问题)。当然也可以想到“打鸟惊群”的函数pthread_cond_broadcast(),打一枪,无论打没打着,一群鸟都飞走了。
  4、有了上面的基础,接下来就重点关注任务部分了。当然线程数量有限,上面已经说了,是固定的数目。因此任务大于线程数时,排队是难免的了。因此创建了一个任务队列,队列中的每一项代表一个任务。任务队列的节点最简单的模型就是一个处理任务的回掉函数void* (*callback_function)(void *arg)。指向函数的指针,参数是个指针,返回值也是个指针。具体的函数和参数则需要另外写函数定义。没次调用完线程处理完这个任务,就需要把它从任务队列中删除。进入任务队列的任务数也不能无限多,因此也设为一个比线程数稍微大个几个的一个固定值。
5、线程动态创建:一个线程退出后(在任务执行失败时,就有可能会退出),主线程要能检测到,然后动态创建一个新的线程,以维持线程池中线程总数不变。可以通过pthread_join()阻塞等待回收子线程资源,但是这就意味着主线程在阻塞状态下干不了其他工作,因此考虑使用线程信号,在子线程结束时,给主线程用pthread_kill()发送一个SIGUSR1信号,在主线程接收到此信号时,通过调用注册函数signal()或sigaction()函数注册的函数创建一个新的线程。
 
下面只列出Threadpool核心的实现,封装条件变量的类在这里没有列出。
//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;              //线程池销毁标志  
};  

// 线程入口函数  
// 这其实就相当于一个消费者线程, 不断的消费任务(执行任务)  
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();  
}  
完整的代码请到我的Github查看: https://github.com/Tachone/LinuxPorgDemo/tree/master/threadpool_C%2B%2B

关于高级线程池的探讨

   简单线程池存在一些问题,比如如果有大量的客户要求服务器为其服务,但由于线程池的工作线程是有限的,服务器只能为部分客户服务,其它客户提交的任务,只能在任务队列中等待处理。一些系统设计人员可能会不满这种状况,因为他们对服务器程序的响应时间要求比较严格,所以在系统设计时可能会怀疑线程池技术的可行性,但是线程池有相应的解决方案。调整优化线程池尺寸是高级线程池要解决的一个问题。主要有下列解决方案:

 

方案一:动态增加工作线程

   在一些高级线程池中一般提供一个可以动态改变的工作线程数目的功能,以适应突发性的请求。一旦请求变少了将逐步减少线程池中工作线程的数目。当然线程增加可以采用一种超前方式,即批量增加一批工作线程,而不是来一个请求才建立创建一个线程。批量创建是更加有效的方式。该方案还有应该限制线程池中工作线程数目的上限和下限。否则这种灵活的方式也就变成一种错误的方式或者灾难,因为频繁的创建线程或者短时间内产生大量的线程将会背离使用线程池原始初衷--减少创建线程的次数。

   举例:Jini中的TaskManager,就是一个精巧线程池管理器,它是动态增加工作线程的。SQL Server采用单进程(Single Process)多线程(Multi-Thread)的系统结构,1024个数量的线程池,动态线程分配,理论上限32767。

方案二:优化工作线程数目

   如果不想在线程池应用复杂的策略来保证工作线程数满足应用的要求,你就要根据统计学的原理来统计客户的请求数目,比如高峰时段平均一秒钟内有多少任务要求处理,并根据系统的承受能力及客户的忍受能力来平衡估计一个合理的线程池尺寸。线程池的尺寸确实很难确定,所以有时干脆用经验值。

   举例:在MTS中线程池的尺寸固定为100。 

方案三:一个服务器提供多个线程池

   在一些复杂的系统结构会采用这个方案。这样可以根据不同任务或者任务优先级来采用不同线程池处理。

   举例:COM+用到了多个线程池。

这三种方案各有优缺点。在不同应用中可能采用不同的方案或者干脆组合这三种方案来解决实际问题。

 

线程池技术适用范围及应注意的问题

下面是我总结的一些线程池应用范围,可能是不全面的。

线程池的应用范围:

   (1)需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

   (2)对性能要求苛刻的应用,比如要求服务器迅速相应客户请求。

   (3)接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并出现"OutOfMemory"的错误。

 

参考:

《UNP》

IBM文档: http://www.ibm.com/developerworks/cn/java/l-threadPool/

http://blog.csdn.net/zjf280441589/article/details/43883137



Linux下简单线程池的实现

标签:

原文地址:http://blog.csdn.net/nk_test/article/details/50835550

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!