2024/11/11-2024/11/:
前段时间跟着教程进行了线程池项目的编写,在此记录实践的过程和思考。
项目模块概览:

之前在linux上做过C语言实现的简单线程池项目,但是在进行C++的线程池编写时,能够比较明显的感觉到用C++实现比C语言方便了很多,运用STL和C++11、17的一些特性,能使线程池的实现更加方便。
在线程池中,主要的数据结构就是存放任务的Task队列和存放线程的Map结构,在map中使用id作为key,thread对象作为value,在后期能够较为方便的对thread对象进行析构。
如果学过操作系统的相关知识,可以看出我们需要做的应该是一个一对一的线程模型,即一个用户线程(我们定义的线程对象)对应一个内核级线程,操作系统只能看见内核级的线程并对其进行线程调度。
明确需求
在进行项目之前,我们首先需要知道项目需要实现什么需求,这里简单写一下伪代码:
ThreadPool pool;
pool.start(int threadNum);
Result res1 = pool.submitTask(Task(Func1, param_list1));
Result res2 = pool.submitTask(Task(Func2, param_list2));
resType ans1 = res1.get();
resType ans2 = res2.get();
线程池当然要实现只有线程池才能做到的任务,即并行的事务处理,我们模仿linux上的C语言多线程编程,大概可以写出这样一个目标需求模板,其中主要涉及的类有ThreadPool类,Thread类,Task类,接收线程执行任务结果的Result类。
模块设计
互斥同步关系分析:
根据上述需求,可以得到以下同步互斥关系:
1、需要ThreadPool中有任务,Thread才可以取出任务;
2、Task队列满时,任务的提交需要等待;
3、需要任务完成后,对应的Result才可以获得结果。
4、ThreadPool中的Task队列需要互斥访问。
这些同步互斥关系需要用互斥锁、信号量以及条件变量来实现。接下来,简单写一下各个类需要实现的功能:
class ThreadPool
初始化
维护thread map以及Task队列
实现Task的提交
在start函数中,根据传入的线程个数参数或者默认的参数来构建thread类对象并对其进行初始化,使各个线程开始运行
向Thread提供ThreadFunc函数以访问其私有成员和正常运行接收任务
class Thread
func_函数用以访问Task队列(因为Task队列是ThreadPool的成员,所以Thread以std::function的形式传入ThreadPool的成员函数作为自己的成员,用以访问ThreadPool的私有成员),func_函数调用初始化时传入的ThreadFunc,而在ThreadFunc中,线程取出任务队列中的任务并执行Task.exec()。
具体来说,在创建线程并将其插入thread map中时,我们可以采用与以下代码类似的操作:
// 线程构造
Thread::Thread(ThreadFunc func)
// std::move作用主要可以将一个左值转换成右值引用
: func_(std::move(func))
, threadId_(generateId_++) {}
...
auto ptr = std::make_unique<Thread>(std::bind(&ThreadPool::threadFunc, this, std::placeholders::_1));
int threadId = ptr->getId();
threads_.emplace(threadId, std::move(ptr));
之后,我们可以在Thread的start()函数中运行传入的ThreadFunc成员:
void Thread::start() {
// 创建一个线程来执行由线程池提供的线程函数
std::thread t(func_, threadId_);
// 设置分离线程,在start结束后继续运行
// 类似于 linux 中的pthread_detach
t.detach();
}
class Task
作为基类,用户编写派生类来覆盖Task中的虚函数run(),做到用户对不同类型任务的自定义:
// 任务抽象基类
class Task {
public:
Task();
~Task() = default;
// 用户可以自定义任意任务类型,从Task继承,重写run方法,实现自定义任务处理
// 希望run返回自定义值不能使用虚函数+模板函数:
// 代码编译时会给当前类产生虚函数表,将虚函数地址记录在虚函数表内,但是模板类型函数未实例化,不存在地址
virtual Any run() = 0;
// 封装run()并根据返回值修改result
void exec();
void setResult(Result* res);
private:
// 不能设置为智能指针,避免出现和Result类的强智能指针交叉引用问题
Result* result_; // result对象的生命周期长于task,且task中不会为该指针分配堆空间,故不用担心析构问题
};
// 派生类例子
class MyTask: public Task {
public:
MyTask(int begin, int end)
: begin_(begin)
, end_(end)
{}
Any run() override {
std::cout << "begin threadFunc tid: " << std::this_thread::get_id() << std::endl;
int sum = 0;
for (int i = begin_; i <= end_; ++i) sum += i;
std::cout << "tid: " << std::this_thread::get_id() << "end!" << std::endl;
return sum;
}
private:
int begin_;
int end_;
};
在这里,如何返回一个自定义的数据类型成为了实现的难点——因为虚函数不可定义为模板函数:
⚠ 每个含有虚函数的类中都有一个虚函数表,该虚函数表存储着该类的所有的虚函数的地址,因此,虚函数为模板函数时,该表的大小是不知道的,因此编译器禁止。
reference C++类中的虚函数不能为模板函数_虚函数可以是模板函数吗-CSDN博客
因此,我们需要使用一个Result类用来返回自定义的值。因为Result和Task应该是一对一的关系,所以我们将线程池的submmitTask函数的返回值定义为Result类型,在其执行完毕后,该返回值会传入Task的setResult函数中,并使用信号量通知Result对象Task运行结束的消息,从而使Result类调用取得结果的get方法时可以被阻塞。
class Result
该类与Task类一对一的绑定关系比较难搞清楚:
Result::Result(std::shared_ptr<Task> task, bool isValid)
: task_(std::move(task))
, isValid_(isValid) {
task_->setResult(this);
}
这里画张图:

如果能明确这几个模块之间的调用关系,就很容易搞明白线程池里面的运行逻辑。
&spm=1001.2101.3001.5002&articleId=143687945&d=1&t=3&u=335e5ee089d6460cba39921a72ae53d0)
391

被折叠的 条评论
为什么被折叠?



