线程池-通过C++实现

基于C++ 实现线程池

用到少量的C++11新特性

使用多线程编程的时候,当我们需要一个线程时就去创建一个线程,这样是非常方便的。

但是会有一个问题:如果并发量很高,且每个线程存活的时间可能很短,这就会导致频繁的创建和销毁线程,从而导致系统效率大大降低。

1 池技术

池技术的出发点都是为了解决资源频繁创建和释放所造成的资源浪费。

简单来说,池技术就是用有个“池”来专门管理资源,需要时从“池”中取(而不是新创建),不需要时放回“池”中(而不是销毁),“池”中始终维持一定数量的资源。这样就可避免因为频繁创建和销毁而造成的资源(如计算资源、时间资源)的浪费。

2 线程池

线程池也属于池技术的一种。

对于线程池而言,线程的创建以及释放都需要消耗一定的系统资源(如计算资源),因此可以通过线程池技术,来避免因为频繁创建和释放线程所造成的资源损失。

3 线程池组成

线程池可以简单看为三个组成部分:

任务队列、管理者线程、工作者线程。

大体思路:

线程池启动时初始化一定数量的工作者线程,并且等待(阻塞)任务队列,直到有任务可以执行(有事可干)。

添加任务时将任务添加到任务队列,并且唤醒等待中的工作者线程。

当任务队列不为空的时候,工作者线程从任务队列中取出一个任务并执行。

当工作者线程不够(或者说是任务很多时)管理者线程按照一定规则添加一定数量的工作者线程。

当闲着的工作者线程比较多的时候,管理者按照一定规则将空闲工作者线程销毁。

以上需要注意保护共享变量的访问,需要加互斥锁:

添加任务、取出任务等

需要添加条件变量:

任务队列不为空时唤醒阻塞的工作者线程;任务队列为空时工作者线程阻塞。

3.1 任务队列
3.1.1 任务Task

首先定义任务,因为任务有function,以及function的执行参数。

为简化,这里前提都把任务的返回值假设为void类型;

// TaskQueue.h	
using callback = void (*)(void*);
struct Task{
    Task(){
        function = nullptr;
        arg = nullptr;
    }
    Task(callback f, void* arg){
        function = f;
        this->arg = arg;
    }
    callback function;
    void* arg;
}
3.1.2 任务队列

这里用STL中的queue来帮忙管理任务队列。

这样的话任务队列的容量无上限:生产者可以一直生产而无需阻塞生产者。

// TaskQueue.h	

class TaskQueue{
private:
	queue<Task> m_taskQ;    
public:
    // 提供一些API来操作任务队列
    TaskQueue();
    ~TaskQueue();
    inline size_t getTaskNums(){
        return m_taskQ.size();
    }
    // 添加任务
    void addTask(Task task);
    void addTask(callback f, void* arg);
    // 取出任务
    int takeTask(Task& task);
    
private:
    pthread_mutex m_mutex; // 因为可能会有多个线程同时访问m_taskQ,所以加互斥锁来同步
    
}
// TaskQueue.cpp
TaskQueue::TaskQueue(){
    // 初始化互斥锁
    pthread_mutex_init(&m_mutex, NULL);
}
TaskQueue::~TaskQueue(){
    // 释放互斥锁
    pthread_mutex_destroy(&m_mutex);
}

// 添加任务
void TaskQueue::addTask(Task task){
    pthread_mutex_lock(&m_mutex); // 加锁
    m_taskQ.push(task);
    pthread_mutex_unlock(&m_mutex); // 解锁
}
void TaskQueue::addTask(callback f, void* arg){
    pthread_mutex_lock(&m_mutex); // 加锁
    m_taskQ.push(Task(f,arg));
    pthread_mutex_unlock(&m_mutex); // 解锁
}

// 取出任务
/*正常取到任务返回0;否则返回-1*/
int TaskQueue::takeTask(Task& task){
    pthread_mutex_lock(&m_mutex); // 加锁
    int res = -1;
    // 首先判断任务队列是否为空
    if(!m_taskQ.empty()){
        task = m_taskQ.front();
        m_taskQ.pop();
        res = 0;
    }
    pthread_mutex_unlock(&m_mutex); // 解锁
    return res;
}
3.2 线程池

管理者线程(1)和工作者线程(N)都放在线程池中进行管理。

线程池需要维护:
任务队列
最小工作者线程数、最大工作者线程数、忙的工作者线程数、存活的工作者线程数、要销毁的工作者线程数

以及互斥锁和条件变量:
任务队列非空的条件变量、对象共享变量访问的互斥锁

3.2.1 声明
// ThreadPool.h

class ThreadPool{
private:
    int minNum;		// 最小工作者线程数
    int maxNum;		// 最大工作者线程数
    int busyNum;	// 忙的工作者线程数
    int liveNum;	// 存活的工作者线程数
    int exitNum;	// 要销毁的工作者线程数
    TaskQueue* taskQ;	// 任务队列
    pthread_t managerID;	// 管理者线程的线程ID
    pthread_t* workerIDs;	// 工作者线程的线程ID数组(队列)
	const static NUMBER = 2;  // 用于控制每次添加、销毁的线程数 MARK 0
public:
    ThreadPool(int min=2, int max=5);
    ~ThreadPool();
    // 公共API
    // 添加任务
    void addTask(Task task);
    // 获取线程池忙的线程个数
    int getBusyNum();
    // 获取线程池存活的线程个数
    int getAliveNum();
private:
    pthread_mutex_t mutexPool;  // 共享资源访问互斥锁
    pthread_cond_x notEmpty;  // 条件变量
    // 不需要给用户访问的方法(类内部调用)
	static void* worker(void* arg); // 工作线程的任务函数  MARK 3
	// 管理者线程任务函数
	static void* manager(void* arg);
	// 单个线程退出
	void threadExit();
}

3.2.2 构造函数
// ThreadPool.cpp
/*MARK 的地方下面会补充注解*/
ThreadPool::ThreadPool(int min, int max):minNum(min), maxNum(max) {
    do{
        // 初始化任务队列
        taskQ = new TaskQueue();
        if(nullptr == taskQ){
            cerr<<" taskQ 内存初始化失败"<<endl;
            break;
        }
        // 初始化工作者线程
        workerIDs = new pthread_t[max]; // 按照maxNum来初始化工作者线程容量数组
        if(nullptr == workerIDs){
            cerr<<" workerIDs 内存初始化失败"<<endl;
            break;
        }
        // 将工作者线程全部清0
        memset(workerIDs, 0, sizeof(pthread_t)*max); // 工作者线程id=0表示没有线程
        this->busyNum = 0;
        this->liveNum = min;
        this->exitNum = 0;
        // 初始化shutDown
		shutDown = false;
        // 创建工作线程
		for (int i = 0; i < min; ++i) {
			pthread_create(&threadIDs[i], NULL, worker, this); // MARK 1
		}
        // 创建管理者线程
        pthread_create(&managerID, NULL, manager, this); // MARK 2
        // 锁和条件变量的初始化
		if (0 != pthread_mutex_init(&mutexPool, NULL) ||
			0 != pthread_cond_init(&notEmpty, NULL)) 
		{
			cout << "锁或条件变量初始化失败" << endl;
			break;
		}
        // 申请成功,直接退出,不再往下执行
        return;
    }while(0);
    // 初始化不成功,将申请的资源全部释放
    if(workerIDs) delete[]workerIDs;
    if(taskQ) delete taskQ;
}





3.2.3 析构函数
// ThreadPool.cpp
ThreadPool::~ThreadPool(){
    //关闭线程池
    this->shutDown = true;
    sleep(3); // 管理者线程中每3s检测一次
    // 阻塞并回收管理者线程
    pthread_join(&managerID, NULL); //  第二个参数用于接收返回值
    // 唤醒阻塞中的工作者线程
    for(int i=0; i<liveNum; ++i){
        // 有多少存活的工作线程就发送多少次唤醒信号,这样就一定能保证所有的工作者线程都被唤醒
        // 由于以上shutDown = true;
        // 那么工作者线程被唤醒后因为shutDown = true 会进行自我销毁;
        pthread_cond_signal(&notEmpty);
    }
    // 堆内存的释放
    if(taskQ) delete taskQ;
    if(workerIDs) delete[]workerIDs;
    
    // 释放互斥锁和条件变量
    pthread_cond_destroy(&notEmpty);
    pthread_mutex_destroy(&mutexPool);
}
3.2.4 添加任务
// ThreadPool.cpp
void addTask(Task task){
    // pthread_mutex_lock(&mutexPool); 这里其实已经不需要锁了,因为在做TaskQueue的时候已经加锁了
    // 判断线程池是否停止工作
    if(this->shutDown){
        //pthread_mutex_unlock(&mutexPool);
        return;
    }
    // 添加任务 
    taskQ.addTask(task);
    // 唤醒因为任务队列为空而被阻塞的工作者进程(生产者生产后唤醒消费者)
    //pthread_mutex_unlock(&mutexPool);
    pthread_cond_signal(&notEmpty);    
}
3.2.5 获取属性

这里可以按照属性被使用时的一些安全方面问题,然后再具体考虑是否需要加锁。

// ThreadPool.cpp
int ThreadPool::getBusyNum()
{
	pthread_mutex_lock(&this->mutexPool);
	int busyN = this->busyNum;
	pthread_mutex_unlock(&this->mutexPool);
	return busyN;
}
int ThreadPool::getAliveNum()
{
	pthread_mutex_lock(&this->mutexPool);
	int liveN = this->liveNum;
	pthread_mutex_unlock(&this->mutexPool);
	return liveN;
}
3.2.6 线程销毁函数threadExit()

进行自我销毁的线程通过调用这个函数来实现销毁。
函数属于类对象的成员函数,工作者线程可以直接访问。
首先需要先获取自己的线程 ID

// ThreadPool.cpp
void ThreadPool::threadExit(){
	pthread_t tid = pthread_self(); // 先获取自己的线程 ID
    for(int i=0; i<maxNum; ++i){
        if(tid != workerIDs[i]) continue;
        workerIDs[i] = 0; // 将这个线程ID置为0(即空)
        if (DEBUG) cout << "threadExit() called, " << to_string(tid) << "exiting...\n";
        break;
    }
}


3.2.7 工作者线程的绑定函数worker()
// ThreadPool.cpp
/*注意:每一个被创建的工作线程都会进来到这个函数*/
void* ThreadPool::worker(void* arg){
    // 注意上面创建工作者线程时传进来的this参数即为一个ThreadPool对象指针:
    /*pthread_create(&threadIDs[i], NULL, worker, this); // MARK 1*/
    
    ThreadPool* pool = static_cast<ThreadPool*>(arg);  // 使用c++ 的强制类型转换
    while(true){ // 重复循环
        pthread_mutex_lock(&pool->mutexPool);  // 加锁:访问到任务队列的任务数防止其他线程添加任务
        // 任务队列不为空且线程池运行
        if( pool->taskQ->getTaskNums() == 0 && !pool->shutDown){
            // 阻塞,等待任务到来或者线程池关闭(阻塞时线程释放锁)
            pthread_cond_wait(&notEmpty, &mutexPool); // 当被唤醒的时候,线程会同时获得锁
            // 被唤醒后:
            // 是否需要进行自我销毁
            if(pool->exitNum>0){
                pool->exitNum--;
                // 如果线程池中存活的工作者线程数大于最小工作者线程数时才实际进行自我销毁
                if(pool->liveNum > pool->minNum){
                    pool->liveNum --;
                    pthread_mutex_unlock(&mutexPool);// 释放锁
                    pool->threadExit();// 线程退出
                }
            }
        }
        // 没有被阻塞的工作者线程继续往下执行:
        // 线程池关闭
        if(pool->shutDown){ 
            pthread_mutex_unlock(&mutexPool);// 释放锁
            pool->threadExit(); // 线程退出
        }
        
        // 否则继续执行:
        // 从任务队列中取出任务
        Task task;
        int res = pool->taskQ->takeTask(task);
        if(res!=-1){ // 正常拿到任务
			if (DEBUG) cout << "thread " << to_string(pthread_self()) 
                << " start working...\n";
        }
        pool->busyNum ++;
        pthread_mutex_unlock(&mutexPool);// 释放锁
        if(-1 != res){
            /*执行任务*/
        	task.function(task.arg);
        	/*任务结束*/
        }
        if (DEBUG) cout << "thread " << to_string(pthread_self()) << " end working...\n";
		pthread_mutex_lock(&pool->mutexPool);
		pool->busyNum--;
		pthread_mutex_unlock(&pool->mutexPool); // 释放锁
    }
    return nullptr;
}
3.2.8 管理者线程绑定函数manager()
// ThreadPool.cpp
// 注意,只有一个管理者线程 
void* ThreadPool::manager(void* arg){
    // 注意上面创建工作者线程时传进来的this参数即为一个ThreadPool对象指针:
    /*pthread_create(&managerID, NULL, manager, this); // MARK 2*/
    
    ThreadPool* pool = static_cast<ThreadPool*>(arg);  // 使用c++ 的强制类型转换
    
    while(!pool->shutDowm){
        sleep(3);// 每隔3s 检测一次
        
        // 获取当前线程池的一些属性,以来判断是否需要添加或销毁工作者线程
        pthread_mutex_lock(&pool->mutexPool); 	 // 加锁
		int busyN = pool->busyNum; 
        int liveN = pool->liveNum;
        int taskN = pool->taskQ->taskNumber();
		pthread_mutex_unlock(&pool->mutexPool); 	// 释放锁
        
        // 添加线程
		// 任务个数 > 存活的线程数 && 存活的线程数 < 最大线程数
        if(taskN > liveN && liveN < pool->maxNum){
            pthread_mutex_lock(&pool->mutexPool);	// 加锁
            for(int i=0, counter=0; i<pool->maxNum && counter<NUMBER 
                && pool->liveNum<pool->maxNum; ++i){
                if(0 == pool->workerIDs[i]) continue;
                pthread_create(&pool->workerIDs[i], NULL, worker, pool);
                counter++;
            }
            pthread_mutex_unlock(&pool->mutexPool); // 释放锁
        }
        // 销毁线程
        // 忙的线程*2 < 存活的线程线程 && 存活的线程 > 最小线程数
        if(busyN*2 < liveN && liveN > pool->minNum){
            pthread_mutex_lock(&pool->mutexPool);	// 加锁
            // 这里是释放条件变量,让阻塞的工作者线程进行自我销毁
            pool->exitNum = NUMBER;
            for(int i=0; i<NUMBER; ++i){
                pthread_cond_signal(&pool-<notEmpty);
            }
            pthread_mutex_unlock(&pool->mutexPool); // 释放锁
        }
    }
    return nullptr;
}


4 MARK

4.0 MARK 0
const static NUMBER = 2;  // 用于控制每次添加、销毁的线程数 MARK 0

因为变量NUMBER是在static类函数中会被使用,且不会对它进行修改,因此设置为const static类型。
这个变量用于控制每次添加、销毁的线程数

4.1 MARK 1
pthread_create(&threadIDs[i], NULL, worker, this); // MARK 1

第三个参数绑定的worker函数不能是类对象的成员函数(即类的普通成员函数);
因为这里需要的是一个函数的指针(即地址),而类对象在没有创建、初始化的时候类成员函数是没有地址的,因此不能这样做。

解决的方法:

  • 声明为类的成员函数,即用static关键字修饰
  • 声明为外部函数且通过friend声明为类的友元函数以方便访问类的相关属性

第四个参数this:

  • 与当前实例化对象绑定
4.2 MARK 2
pthread_create(&managerID, NULL, manager, this); // MARK 2

同4.2

4.3 MARK 3
static void* worker(void* arg); // 工作线程的任务函数  MARK 3

见4.2

5 测试程序

// test.cpp

#include "unistd.h"
#include <iostream>

#include "ThreadPool.h"

using namespace std;


void taskFunc(void* arg) {
	int num = *(int*)arg;
	cout << "thread " << to_string(pthread_self()) << " is working, number= " << num << endl;
	sleep(1);
}


int main() {
	cout << "main function\n";
	ThreadPool pool(3,10);

	for (int i = 0; i < 100; ++i) {
		int* num = new int(i + 100);
		pool.addTask(Task(taskFunc, num));
	}

	sleep(20);
	cout << "end main\n";
	return 0;
}



6 测试样例结果

main function
thread 140412891649792 start working...
thread 140412891649792 is working, number= 100
thread 140412883257088 start working...
thread 140412883257088 is working, number= 101
thread 140412874864384 start working...
thread 140412874864384 is working, number= 102
thread 140412891649792 end working...
thread 140412891649792 start working...
thread 140412891649792 is working, number= 103
thread 140412883257088 end working...
thread 140412883257088 start working...
thread 140412883257088 is working, number= 104
thread 140412874864384 end working...
thread 140412874864384 start working...
thread 140412874864384 is working, number= 105
thread 140412891649792 end working...
thread 140412891649792 start working...
thread 140412891649792 is working, number= 106
thread 140412883257088 end working...
thread 140412883257088 start working...
thread 140412883257088 is working, number= 107
thread 140412874864384 end working...
thread 140412874864384 start working...
thread 140412874864384 is working, number= 108
thread 140412866471680 start working...
thread 140412866471680 is working, number= 109
thread 140412858078976 start working...
thread 140412858078976 is working, number= 110
thread 140412891649792 end working...
thread 140412891649792 start working...
...
...
thread 140412849686272 end working...
thread 140412849686272 start working...
thread 140412849686272 is working, number= 194
thread 140412841293568 end working...
thread 140412841293568 start working...
thread 140412841293568 is working, number= 195
thread 140412866471680 end working...
thread 140412866471680 start working...
thread 140412866471680 is working, number= 196
thread 140412858078976 end working...
thread 140412858078976 start working...
thread 140412858078976 is working, number= 197
thread 140412891649792 end working...
thread 140412891649792 start working...
thread 140412891649792 is working, number= 198
thread 140412883257088 end working...
thread 140412883257088 start working...
thread 140412883257088 is working, number= 199
thread 140412874864384 end working...
thread 140412345906944 end working...
thread 140412832900864 end working...
thread 140412824508160 end working...
thread 140412849686272 end working...
thread 140412841293568 end working...
thread 140412866471680 end working...
thread 140412858078976 end working...
thread 140412891649792 end working...
thread 140412883257088 end working...
end main

可以看到,最后一共是创建了10个线程。

7 TODO

基于C++11的新特性的版本

  • future
  • packaged_task
  • bind
  • std::mutex

8 Thanks

线程池工作原理和实现-【C语言改C++版】 C/C++_哔哩哔哩_bilibili

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值