C++项目实践记录:线程池项目复盘(仅思路)

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);
}

        这里画张图:

        如果能明确这几个模块之间的调用关系,就很容易搞明白线程池里面的运行逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值