线程池的技术背景

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在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, 请移步于此, 向您郑重推荐, 这是一篇非常好的文章, 谢谢!

Linux多线程实践(9) --简单线程池的设计与实现的更多相关文章

  1. Linux下简单线程池的实现

    大多数的网络服务器,包括Web服务器都具有一个特点,就是单位时间内必须处理数目巨大的连接请求,但是处理时间却是比较短的.在传统的多线程服务器模型中是这样实现的:一旦有个服务请求到达,就创建一个新的服务 ...

  2. C#多线程之旅(3)——线程池

    v博客前言 先交代下背景,写<C#多线程之旅>这个系列文章主要是因为以下几个原因:1.多线程在C/S和B/S架构中用得是非常多的;2.而且多线程的使用是非常复杂的,如果没有用好,容易造成很 ...

  3. 基于C++11的100行实现简单线程池

    基于C++11的100行实现简单线程池 1 线程池原理 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务.线程池线程都是后台线程.每个线程都使用默认的堆栈大小, ...

  4. java核心-多线程(6)-线程池-ThreadPoolExecutor

    1.java多线程编程少不了使用线程池,线程池相关的工具类所在jdk包,java.util.concurrent 2.使用示例 demo1 public class ThreadPoolDemo { ...

  5. 第十章:Python高级编程-多线程、多进程和线程池编程

    第十章:Python高级编程-多线程.多进程和线程池编程 Python3高级核心技术97讲 笔记 目录 第十章:Python高级编程-多线程.多进程和线程池编程 10.1 Python中的GIL 10 ...

  6. C# 多线程的自动管理(线程池) 基于Task的方式

    C# 多线程的自动管理(线程池) 在多线程的程序中,经常会出现两种情况:    1. 应用程序中线程把大部分的时间花费在等待状态,等待某个事件发生,然后给予响应.这一般使用 ThreadPool(线程 ...

  7. 多线程(七)JDK原生线程池

    如同数据库连接一样,线程的创建.切换和销毁同样会耗费大量的系统资源.为了复用创建好的线程,减少频繁创建线程的次数,提高线程利用率可以引用线程池技术.使用线程池的优势有如下几点:        1.保持 ...

  8. 基于Linux/C++简单线程池的实现

    我们知道Java语言对于多线程的支持十分丰富,JDK本身提供了很多性能优良的库,包括ThreadPoolExecutor和ScheduleThreadPoolExecutor等.C++11中的STL也 ...

  9. linux下c语言实现简单----线程池

    这两天刚好看完linux&c这本书的进程线程部分,学长建议可以用c语言实现一个简单的线程池,也是对线程知识的一个回顾与应用.线程的优点有好多,它是"轻量级的进程",所需资源 ...

随机推荐

  1. SUSE11虚拟机安装与Oracle 11g安装

    SUSE11虚拟机安装与Oracle 11g安装 本文中所需所有参数均位于文末附录中 新建虚拟机,选择SUSE11 64位 启动虚拟机后,选择第二项安装 选择语言 跳过CD检查 选择全新安装 选择默认 ...

  2. ACM Max Factor

    To improve the organization of his farm, Farmer John labels each of his N (1 <= N <= 5,000) co ...

  3. k8s Kubernetes v1.10 最简易安装 shell

    k8s Kubernetes v1.10 最简易安装 shell # Master 单节点快速安装 # 最简单的安装shell,只为快速部署k8s测试环境 #环境centos 7.4 #1 初始化环境 ...

  4. NLP系列(2)_用朴素贝叶斯进行文本分类(上)

    作者:龙心尘 && 寒小阳 时间:2016年1月. 出处: http://blog.csdn.net/longxinchen_ml/article/details/50597149 h ...

  5. sublime text 2 解决错误 [Decode error - output not utf-8]

    以win 10 为例, 找到文件C:\Users\xxzx\AppData\Roaming\Sublime Text 2\Packages\Python\Python.sublime-build 添加 ...

  6. 聚沙成塔-linux 常用命令

    批量更改文件后缀名 find . -depth -name "*.scss" -exec sh -c 'mv "$1" "${1%.scss}.les ...

  7. JavaSE基础问答

    1.JAVA的基本数据类型有哪些? JAVA的基本数据类型分为4类8种,就是整形 byte.short.int.long.浮点型 float 跟double,字符型 char,跟布尔型 true和 f ...

  8. Ruby 连接MySQL数据库

    使用Ruby连接数据库的过程还真的是坎坷,于是写点文字记录一下. 简介 Ruby简介 RubyGems简介 包管理之道 比较著名的包管理举例 细说gem 常用的命令 准备 驱动下载 dbi mysql ...

  9. Python 描述符 data 和 non-data 两种类型

    仅包含__get__的,是non-data descriptor, 如果实例__dict__包含同名变量, 则实例优先; 如果还包含__set__, 则是data descriptor, 优先于实例_ ...

  10. 用reg文件把便携版sublime text 3添加到右键菜单

    假设sublime文件夹在C:\\Users\\T430i\\Downloads\\Sublime Text Build 3059 x64\\ 则: Windows Registry Editor V ...