项目学习地址:【牛客网C++服务器项目学习】
花了三小时,搞定了线程池类的编写,实际测试过了,能够正常跑通。性能测试还没去做,不知道和传统的【即时创建、即时销毁】机制比起来能够快多少。
写这个程序还是踩了一些坑,我把我写程序遇到的问题总结一下:
- 使用模板类编写省心。最开始我不愿意使用模板类进行线程池类的开发,想着说用不上。不过很快就遇到了问题:
- 向线程池中添加一个任务——函数,通常需要用结构体封装一下。因为函数做为参数,表达式太长了,有些许不方便。但是将函数封装在结构中,也带来了新的问题:不能将结构体中的成员函数进行修改,所以啊,还是用模板编程方便一些。
void * func(void *arg) // 这是有一个任务函数
// 封装在结构体中
struct Job
{
void * func(void *arg);
};
// 无法修改结构体中的成员函数
Job *newjob = new Job();
newjob->func = anotherfunc; // 编译器会报错:函数只能调用,不能修改
- 不需要封装【同步线程】类:这是我在编写程序时遇到的另一个问题。【牛客网】视频中将三种线程同步机制做了封装,但是,但是,但是。我最爱用的信号量,我想在类中进行含参的构造函数初始化,编译器会报错!。最后我放弃使用编写好的
Locker
类,直接调用信号量函数还方便些。
- 更改线程同步机制:【牛客网】视频中,他使用了一个互斥锁保证共享资源一次只能有一个线程进行读写访问,还使用了一个信号量表征任务job队列中的资源个数。但这不是很标准的线程同步机制上锁的方式啊。正确的姿势是应该使用信号量创建三个变量:full, empty, mutex分别表示可用资源、空闲槽位、互斥锁。并且加锁和解锁的方式也是固定的,我已经在我V3版本的代码实现了。
- 新的线程同步机制会带来什么好处:
- 第一:规范标注,不会出现意想不到的情况;
- 第二:最重要的一点,大家请去思考【原视频】中的
append
函数,如果任务队列满了之后,它不是去阻塞append
函数,而是直接返回。如果在调用append
函数时,没有根据append
的返回值进行对应的处理,就会丢失当前的任务。
- 新的线程同步机制会带来什么好处:
- 注意各种地址。这个程序有些变量在堆区,很多变量在栈区。同时注意应该是传入地址还是传入数值。
talk is cheap, show me the code:
// 封装一个【线程池】类
#ifndef __THREADPOOLV3_H__
#define __THREADPOOLV3_H__
// 导入头文件
#include <queue>
#include <stdio.h>
#include <semaphore.h>
// 类内声明,类外定义
template <typename T>
class ThreadPool
{
public:
// 构造函数
ThreadPool(unsigned int t_nums, unsigned int j_nums);
// 析构函数
~ThreadPool();
// 成员函数:往线程池中【添加】任务事件
bool append(T *newjob);
private:
// 核心线程的数量
unsigned int m_thread_num;
// 最大线程的数量
unsigned int m_thread_max;
// 任务队列长度
unsigned int m_queue_len;
// 工作队列(存放线程的)
pthread_t *m_work_threads; // 用动态数组实现
// 任务队列(存放线程需要执行的任务《函数》的)
std::queue<T *> m_job_queue;
// 用queue,list,数组实现都行
// 线程的回调函数
static void *worker(void *arg);
void m_run(); // worker的封装函数
void m_add(T *newjob); // append的封装函数
// 线程同步机制
sem_t sem_job_src; // 任务队列资源的个数
sem_t sem_job_empty; //任务队列空闲位置的个数
sem_t sem_mutex; // 互斥锁信号量
// 设置一个标志位,表示整个线程池是否在进行中
bool isRun;
};
/********************** 以下全是类的定义 **********************/
// 构造函数
template <typename T>
ThreadPool<T>::ThreadPool(unsigned int t_nums, unsigned int j_nums)
{
// 初始化线程数量等成员变量
// 判断输入数据的合法性
if (t_nums <= 0 || j_nums <= 0)
{
printf("参数传入错误\n");
throw std::exception();
}
m_thread_num = t_nums;
m_queue_len = j_nums;
// 初始化信号量,表示job队列资源的信号量不变,互斥锁的信号量初始化为1
sem_init(&sem_job_src, 0 , 0);
sem_init(&sem_job_empty, 0, m_queue_len);
sem_init(&sem_mutex, 0, 1);
// 初始化线程池状态
isRun = true;
// 申请堆区内存,存放子线程的线程号
m_work_threads = new pthread_t[m_thread_num];
if (!m_work_threads)
{
// 如果开辟堆区动态内存失败,抛出异常
isRun = false;
printf("堆区开辟内存失败\n");
throw std::exception();
}
// 创建 m_thread_num 个子线程
for (int i = 0; i < m_thread_num; ++i)
{
int ret;
ret = pthread_create(m_work_threads + i, NULL, worker, this);
if (ret != 0)
{
// 创建线程出现异常,终止整个程序,清除资源
delete[] m_work_threads;
isRun = false;
printf("创建线程失败\n");
throw std::exception();
}
}
// 线程创建后,设置线程分离
for (int i = 0; i < m_thread_num; ++i)
{
int ret;
ret = pthread_detach(m_work_threads[i]);
if (ret != 0)
{
// 创建线程出现异常,终止整个程序,清除资源
delete[] m_work_threads;
isRun = false;
printf("线程分离失败\n");
throw std::exception();
}
}
}
// 析构函数
template <typename T>
ThreadPool<T>::~ThreadPool()
{
// 销毁指针,释放堆区内存等
delete[] m_work_threads;
isRun = false;
}
// public成员函数:append
template <typename T>
bool ThreadPool<T>::append(T *newjob)
{
printf("添加任务\n");
m_add(newjob);
return true;
}
template <typename T>
void ThreadPool<T>::m_add(T *newjob)
{
// 往内存池中,添加一个工作事件,事件应该被添加到任务队列中
// 在主线程中,向任务队列中写入数据,必须要加锁
// 上互斥锁
// 用信号量判断job队列是否还有空间
sem_wait(&sem_job_empty); // 工作队列满了,阻塞在此
sem_wait(&sem_mutex);
m_job_queue.push(newjob);
sem_post(&sem_mutex); // 解锁
sem_post(&sem_job_src); // job资源的信号量加1
}
// private成员函数:m_run
template <typename T>
void *ThreadPool<T>::worker(void *arg)
{
// 内存池中的线程,需要执行的任务,任务从任务队列中取
// 从任务队列中取出一个任务,从工作队列(线程s)中取出一个线程,让线程去执行这个任务(函数)
// 任务的形态应该是什么:
// 《函数》
// 其实本质上,仍旧是生产者和消费者模型
printf("执行工作任务\n");
ThreadPool *pool = (ThreadPool *)arg;
pool->m_run();
return NULL;
}
template <typename T>
void ThreadPool<T>::m_run()
{
while (isRun)
{
// 消耗一个资源,如果任务队列没有资源,阻塞等待(就相当于是让线程睡眠了)
sem_wait(&sem_job_src);
sem_wait(&sem_mutex); // 互斥锁
// 取出一个任务
T *Newjob = m_job_queue.front();
m_job_queue.pop();
// 退出,解锁
sem_post(&sem_mutex);
sem_post(&sem_job_empty);
// 拿到job后,在锁外执行job内具体的函数
printf("线程成功获取任务\n");
Newjob->func();
}
}
#endif
测试这个头文件的函数:
#include <iostream>
#include <unistd.h>
#include "threadpoolV3.h"
class Job
{
public:
Job(){}
Job(int *a)
{
arg = new int(*a);
}
// 发给线程的回调函数
void *func()
{
printf("%d\n", *arg);
}
~Job()
{
delete arg;
}
private:
int *arg;
};
int main()
{
// 创建一个线程池对象,线程数量4,任务队列长度为20
ThreadPool<Job> *pool = new ThreadPool<Job>(4,20);
printf("线程池创建成功\n");
int i;
Job *newjob;
for(i = 0;i<100;++i)
{
newjob = new Job(&i);
pool->append(newjob);
}
printf("end\n");
pthread_exit(NULL); // 主线程结束
return 0;
}
贴一下测试结果: