假设服务器的硬件资源“充裕”,那么提高服务器性能的一个很直接的方法就是空间换时间,即“浪费”服务器的硬件资源,以换取其运行效率。提升服务器性能的一个重要方法就是采用“池”的思路,即对一组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源分配。当服务器进入正式运行阶段,即开始处理客户端请求时,如果它需要相关资源就可以直接从池中获取,无需动态分配。很显然,直接从池中取得所需要资源比动态分配资源的速度快得多,因为分配系统资源的系统调用都是很耗时的。当服务器处理完一个客户端连接后,可以把相关资源放回池中,无须执行系统调用释放资源。从最终效果来看,资源分配和回收的系统调用只发生在服务器的启动和结束,这种“池”的方式避免了中间的任务处理过程对内核的频繁访问,提高了服务器的性能。我们常用的线程池和内存池都是基于以上“池”的优势所设计出来的提升服务器性能的方法,今天打算以C++98设计一个基于Linux系统的简单线程池。

为什么要采用线程池?

首先想一想,我们一般的服务器都是动态创建子线程来实现并发服务器的,比如每当有一个客户端请求建立连接时我们就动态调用pthread_create去创建线程去处理该连接请求。这种模式有什么缺点呢?

  • 动态创建线程是比较费时的,这将到导致较慢的客户响应。
  • 动态创建的子线程通常只用来为一个客户服务,这将导致系统上产生大量的细微线程,线程切换也会耗费CPU时间。

所以我们为了进一步提升服务器性能,可以采取“池”的思路,把线程的创建放在程序的初始化阶段一次完成,这就避免了动态创建线程导致服务器响应请求的性能下降。

线程池的设计思路

  1. 以单例模式设计线程池,保证线程池全剧唯一;
  2. 在获取线程池实例进行线程池初始化:线程预先创建+任务队列创建;
  3. 创建一个任务类,我们真实的任务会继承该类,完成任务执行。

根据以上思路我们可以给出这么一个线程池类的框架:

class ThreadPool
{
private:
std::queue<Task*> taskQueue; //任务队列
bool isRunning; //线程池运行标志
pthread_t* pThreadSet; //指向线程id集合的指针
int threadsNum; //线程数目
pthread_mutex_t mutex; //互斥锁
pthread_cond_t condition; //条件变量 //单例模式,保证全局线程池只有一个
ThreadPool(int num=10);
void createThreads(); //创建内存池
void clearThreads(); //回收线程
void clearQueue(); //清空任务队列
static void* threadFunc(void* arg);
Task* takeTask(); //工作线程获取任务 public:
void addTask(Task* pTask); //任务入队
static ThreadPool* createThreadPool(int num=10); //静态方法,用于创建线程池实例
~ThreadPool();
int getQueueSize(); //获取任务队列中的任务数目
int getThreadlNum(); //获取线程池中线程总数目 };

下面开始讲解一些实现细节。

1.单例模式下的线程池的初始化

首先我们以饿汉单例模式来设计这个线程池,以保证该线程池全局唯一:

  1. 构造函数私有化
  2. 提供一个静态函数来获取线程池对象
//饿汉模式,线程安全
ThreadPool* ThreadPool::createThreadPool(int num)
{
static ThreadPool* pThreadPoolInstance = new ThreadPool(num);
return pThreadPoolInstance;
} ThreadPool* pMyPool = ThreadPool::createThreadPool(5);

线程池对象初始化时我们需要做三件事:相关变量的初始化(线程池状态、互斥锁、条件变量等)+任务队列的创建+线程预先创建

ThreadPool::ThreadPool(int num):threadsNum(num)
{
printf("creating threads pool...\n");
isRunning = true;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condition, NULL);
createThreads();
printf("created threads pool successfully!\n");
}

线程池的数目根据对象创建时输入的数目来创建,如果不指定数目,我们就是使用默认数目10个。

void ThreadPool::createThreads()
{
pThreadSet = (pthread_t*)malloc(sizeof(pthread_t) * threadsNum);
for(int i=0;i<threadsNum;i++)
{
pthread_create(&pThreadSet[i], NULL, threadFunc, this);
}
}

2.任务添加和线程调度

对于每一个服务请求我们都可以看作是一个任务,一个任务来了我们就将它送进线程池中的任务队列中,并通过条件变量的方式通知线程池中的空闲线程去拿任务去完成。那问题来了,这里的任务在编程的层面上看到底是什么?我们可以将任务看成是一个回调函数,将要执行的函数指针往任务队列里面送就可以了,我们线程拿到这个指针后运行该函数就等于完成服务请求。基于以上的考虑,我们设计了一个单独的抽象任务类,让子类继承。类里面有个纯虚函数run(),用于执行相应操作。

考虑到回调函数需要传参数进来,所以特意设置了个指针arg来存储参数地址,到时候我们就可以根据该指针解析出传入的函数实参是什么了。

任务基类

class Task
{
public:
Task(void* a = NULL): arg(a)
{ } void SetArg(void* a)
{
arg = a;
} virtual int run()=0; protected:
void* arg; };
typedef struct
{
int task_id;
std::string task_name;
}msg_t; class MyTask: public Task
{
public:
int run()
{
msg_t* msg = (msg_t*)arg;
printf("working thread[%lu] : task_id:%d task_name:%s\n", pthread_self(),
msg->task_id, msg->task_name.c_str());
sleep(10);
return 0;
}
};

真正使用该类时就自己定义一个子类继承Task类,并实现run()函数,并通过SetArg()方法去设置传入的参数。比如可以这么用:

msg_t msg[10];
MyTask task_A[10]; //模拟生产者生产任务
for(int i=0;i<10;i++)
{
msg[i].task_id = i;
sprintf(buf,"qq_task_%d",i);
msg[i].task_name = buf;
task_A[i].SetArg(&msg[i]);
pMyPool->addTask(&task_A[i]);
sleep(1);
}

现在来到线程池设计中最难搞的地方:线程调度。一个任务来了,究竟怎么让空闲线程去拿任务去做呢?我们又如何保证空闲的线程不断地去拿任务呢?

抽象而言,这是一个生产者消费者的模型,系统不断往任务队列里送任务,我们通过互斥锁和条件变量来控制任务的加入和获取,线程每当空闲时就会去调用takeTask()去拿任务。如果队列没任务那么一些没获得互斥锁的线程就会拥塞等待(因为没锁),获得互斥锁的那个线程会因为没任务而拥塞等待。一旦有任务就会唤醒这个带锁线程拿走任务释放互斥锁。看看代码层面是如何操作的:

加入一个任务

void ThreadPool::addTask(Task* pTask)
{
pthread_mutex_lock(&mutex);
taskQueue.push(pTask);
printf("one task is put into queue! Current queue size is %lu\n",taskQueue.size());
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&condition);
}

取走一个任务

Task* ThreadPool::takeTask()
{
Task* pTask = NULL;
while(!pTask)
{
pthread_mutex_lock(&mutex);
//线程池运行正常但任务队列为空,那就等待任务的到来
while(taskQueue.empty() && isRunning)
{
pthread_cond_wait(&condition, &mutex);
} if(!isRunning)
{
pthread_mutex_unlock(&mutex);
break;
}
else if(taskQueue.empty())
{
pthread_mutex_unlock(&mutex);
continue;
} pTask = taskQueue.front();
taskQueue.pop();
pthread_mutex_unlock(&mutex); } return pTask;
}

线程中的回调函数。这里注意的是,如果取到的任务为空,我们认为是线程池关闭的信号(线程池销毁时我们会在析构函数中调用pthread_cond_broadcast(&condition)来通知线程来拿任务,拿到的当然是空指针),我们退出该线程。

void* ThreadPool::threadFunc(void* arg)
{
ThreadPool* p = (ThreadPool*)arg;
while(p->isRunning)
{
Task* task = p->takeTask();
//如果取到的任务为空,那么我们结束这个线程
if(!task)
{
//printf("%lu thread will shutdown!\n", pthread_self());
break;
} printf("take one...\n"); task->run();
}
}

3.使用例子和测试

下面给出一个线程池的一个使用例子。可以看出,我首先定义了msg_t的结构体,这是因为我们的服务响应函数是带参数的,所以我们定义了这个结构体并把其地址作为参数传进线程池中去(通过SetArg方法)。然后我们也定义了一个任务类MyTask继承于Task,并重写了run方法。我们要执行的服务函数就可以写在run函数之中。当需要往任务队列投放任务时调用addTask()就可以了,然后线程池会自己安排任务的分发,外界无须关心。所以一个线程池执行任务的过程可以简化为:createThreadPool() -> SetArg() -> addTask -> while(1) -> delete pMyPool

#include <stdio.h>
#include "thread_pool.h"
#include <string>
#include <stdlib.h> typedef struct
{
int task_id;
std::string task_name;
}msg_t; class MyTask: public Task
{
public:
int run()
{
msg_t* msg = (msg_t*)arg;
printf("working thread[%lu] : task_id:%d task_name:%s\n", pthread_self(),
msg->task_id, msg->task_name.c_str());
sleep(10);
return 0;
}
}; int main()
{
ThreadPool* pMyPool = ThreadPool::createThreadPool(5);
char buf[32] = {0}; msg_t msg[10];
MyTask task_A[10]; //模拟生产者生产任务
for(int i=0;i<10;i++)
{
msg[i].task_id = i;
sprintf(buf,"qq_task_%d",i);
msg[i].task_name = buf;
task_A[i].SetArg(&msg[i]);
pMyPool->addTask(&task_A[i]);
sleep(1);
} while(1)
{
//printf("there are still %d tasks need to process\n", pMyPool->getQueueSize());
if (pMyPool->getQueueSize() == 0)
{
printf("Now I will exit from main\n");
break;
} sleep(1);
} delete pMyPool;
return 0;
}

程序具体运行的逻辑是,我们建立了一个5个线程大小的线程池,然后我们又生成了10个任务,往任务队列里放。由于线程数小于任务数,所以当每个线程都拿到自己的任务时,任务队列中还有5个任务待处理,然后有些线程处理完自己的任务了,又去队列里取任务,直到所有任务被处理完了,循环结束,销毁线程池,退出程序。

完整的线程池框架和测试例子在我的github

Linux编程之内存池的设计与实现(C++98)的更多相关文章

  1. Linux简易APR内存池学习笔记(带源码和实例)

    先给个内存池的实现代码,里面带有个应用小例子和画的流程图,方便了解运行原理,代码 GCC 编译可用.可以自己上网下APR源码,参考代码下载链接: http://pan.baidu.com/s/1hq6 ...

  2. Nginx系列三 内存池的设计

    Nginx的高性能的是用非常多细节来保证,epoll下的多路io异步通知.阶段细分化的异步事件驱动,那么在内存管理这一块也是用了非常大心血.上一篇我们讲到了slab分配器,我们能够能够看到那是对共享内 ...

  3. linux编程之内存映射

    一.概述                                                   内存映射是在调用进程的虚拟地址空间创建一个新的内存映射. 内存映射分为2种: 1.文件映射 ...

  4. 极高效内存池实现 (cpu-cache)

    视频请看 : http://edu.csdn.net/course/detail/627 1.内存池的目的 提高程序的效率 减少运行时间 避免内存碎片 2.原理   要解决上述两个问题,最好的方法就是 ...

  5. Linux设备驱动--内存管理

           MMU具有物理地址和虚拟地址转换,内存访问权限保护等功能.这使得Linux操作系统能单独为每个用户进程分配独立的内存空间并且保证用户空间不能访问内核空间的地址,为操作系统虚拟内存管理模块 ...

  6. 高效内存池的设计方案[c语言]

    一.前言概述 本人在转发的博文<内存池的设计和实现>中,详细阐述了系统默认内存分配函数malloc/free的缺点,以及进行内存池设计的原因,在此不再赘述.通过对Nginx内存池以及< ...

  7. linux内存源码分析 - 内存池

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 内存池是用于预先申请一些内存用于备用,当系统内存不足无法从伙伴系统和slab中获取内存时,会从内存池中获取预留的 ...

  8. Linux 内存池【转】

    内存池(Memery Pool)技术是在真正使用内存之前,先申请分配一定数量的.大小相等(一般情况下)的内存块留作备用.当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存 ...

  9. linux下C语言实现的内存池【转】

    转自:http://blog.chinaunix.net/uid-28458801-id-4254501.html 操作系统:ubuntu10.04 前言:     在通信过程中,无法知道将会接收到的 ...

随机推荐

  1. 看到一个对CAP简单的解释

    一个分布式系统里面,节点组成的网络本来应该是连通的.然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域.数据就散布在了这些不连通的区域中.这就叫分区.当你一个数据项只在一个节点中 ...

  2. c# 字符串的内存分配和驻留池( 转 )

    刚开始学习C#的时候,就听说CLR对于String类有一种特别的内存管理机制:有时候,明明声明了两个String类的对象,但是他们偏偏却指向同一个实例.如下: string s1 = "he ...

  3. j2ee基础(1)servlet的生命周期

    Servlet的生命周期 Servlet 生命周期规定了 Servlet 如何被加载.实例化.初始化. 处理客户端请求,以及何时结束服务. 该生命周期可以通过 javax.servlet.Servle ...

  4. 配置ssh无密钥登陆

    ssh 无密码登录要使用公钥与私钥. linux下可以用用ssh-keygen生成公钥/私钥对,下面以CentOS为例. 有机器LxfN1(192.168.136.128),LxfN2(192.168 ...

  5. OAuth2.0学习(1-9)新浪开放平台微博认证-web应用授权(授权码方式)

    1. 引导需要授权的用户到如下地址: URL 1 https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&respons ...

  6. maven入门(10)maven的仓库

    [0]README 1)本文部分文字转自 "maven实战",旨在 review  "maven(6)仓库" 的相关知识:   [1]何为 Maven仓库 1) ...

  7. Python模块 - os , sys.shutil

    os 模块是与操作系统交互的一个接口 os.getcwd() 获取当前工作目录,即当前python脚本工作的目录路径 os.chdir("dirname") 改变当前脚本工作目录: ...

  8. Python/零起点(一、数字及元组)

    Python/零起点(一.数字及元组) int整型 int()强行转换成整型数据类型 int整型是不可变,且是不可迭代的对象 一.整型数字用二进制位数表示案例: age=7 #设定一个数字赋值给age ...

  9. [论文阅读] Deep Residual Learning for Image Recognition(ResNet)

    ResNet网络,本文获得2016 CVPR best paper,获得了ILSVRC2015的分类任务第一名. 本篇文章解决了深度神经网络中产生的退化问题(degradation problem). ...

  10. Java 内部类示例

    在下面的示例中,创建了一个数组,使用升序的整数初始化它,并打印索引为偶数的数组值. public class DataStructure { // 创建一个数组 private final stati ...