C++开源库ThreadPool源码解析

目录

构造函数

析构函数

enqueue()函数

 为什么需要lambda表达式呢?

为什么一定要用智能指针shared_ptr<>来管理这个std::packged_task<>对象,我就想用裸指针行不行?

为什么一定要把这个指针(无论是智能指针还是裸指针)封装起来(通过函数封装或者lambda)?


ThreadPool开源库仅用100行代码,运用许多C11特性实现多线程。本文将梳理自己阅读过程中的理解逐行进行解释,并且给出了所有可能疑问的对应答案。

ThreadPool项目Github地址:https://github.com/progschj/ThreadPool

  • 构造函数

inline ThreadPool::ThreadPool(size_t threads)
    : stop(false)
{
    for (size_t i = 0; i < threads; ++i)
        workers.emplace_back(
            [this]
            {
                for (;;) // threads个线程都在异步地无限循环接收任务队列中的任务
                {
                    std::function<void()> task; 
                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock,
                            [this] { return this->stop || !this->tasks.empty(); });
                        if (this->stop && this->tasks.empty())
                            return;
                        task = this->tasks.front();
                       task = std::move(this->tasks.front()); 
                        this->tasks.pop();                     
                    }
                    task();
                }
            }
            );
}
  1.  workers作为存放std::thread对象的vector容器,通过一个for循环及i变量的控制实现创建指定数量的线程
  2. 每个std::thread线程接收一个lambda表达式,此处使用lambda可以便捷地捕获this指针并访问对象中用于线程同步的变量,当然也可以通过函数实现,然后在函数参数中用std::bind()捕获上下文中的变量。
  3. lambda表达式中,无限循环(具体通过for(;;))接收任务。task声明为std::function<void()>类型,这是ThreadPool设计的精妙一处,后文在enqueue()函数的讲解中会说明为什么可以声明成这个类型?如果不想这么做是否有替代方案?
  4. 而后,创建一个{}作用域,用于unique_lock的生命周期。详细注释如下
std::unique_lock<std::mutex> lock(this->queue_mutex); // 获取锁, 如果获取失败阻塞
                                                      // 进程等待
/*
函数原型: cv.wait(lock, predicate)
在等待时自动释放互斥锁,并阻塞当前线程,直到收到通知并且谓词predicate 返回 true。然后它
重新获取锁并继续执行。predicate 返回 true。然后它重新获取锁并继续执行。此处只有
“成员变量stop(ThreadPool对象因某种原因调用了析构函数。)被设置为true || 队列中有任务了”
才会继续执行,否则就算获取到了锁,继续执行可能会遇到任务队列中并没有任务可获取,出现异常。
*/
this->condition.wait(lock,  
      [this] { return this->stop || !this->tasks.empty(); });
if (this->stop && this->tasks.empty()) // stop被设置为true 并且 没有任务可以继续获
   return;                             // 取并执行了直接return退出循环,同时意味着
                                       // 当前线程结束生命周期。             
                            

     5.执行到task = std::move(this->tasks.front()); this->tasks.pop(); 这语句时,说明任务队列中仍然有任务需要处理,接收任务(通过移动语义的方式,避免额外的拷贝),弹出任务队列中的对应任务。

    6. 执行到task();语句:unique_lock<>生命周期结束,互斥锁正常释放。task接收一个可调用的对象,通过task()调用对应任务。

  • 析构函数

inline ThreadPool::~ThreadPool() // 这里的inline只是建议,具体是否为内联取决于编译器
{
    {  // 通过{}维护unique_lock声明周期
        std::unique_lock<std::mutex> lock(queue_mutex); // 获取互斥锁
        stop = true; // 标记设为true, 让enqueue()停止,同时通知线程任务做完该结束啦!
    }
    condition.notify_all(); // 唤醒所有等待中的线程
    for (std::thread& worker : workers)
        worker.join(); // 等待所有线程完成对应的任务直到任务队列为空
}

 

  • enqueue()函数

template<class F, class... Args>  // class 或者 typename都可以,依据个人习惯
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type; 
    auto task = std::make_shared<std::packaged_task<return_type()> >(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );

    std::future<return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex);

        // don't allow enqueueing after stopping the pool
        if (stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");
        tasks.emplace([task]() { (*task)(); });  
    }
    condition.notify_one(); 
    return res;
}
  1. 总体上来说,enqueue 采用模版以及参数包实现接收任意参数的可调用对象。并且对象用std::packaged_task<>封装,实现异步调用。同时保存std::future<>用于未来结果的获取。
  2. 函数返回类型设为auto与尾置类型推导相结合实现不同类型对象的调用。(注意,std::result_of<>已在C14中被弃用,对应的由std::invoke_result<>接替,具体用法自行查找。)
  3. 通过下述语句:
    auto task = std::make_shared<std::packaged_task<return_type()> >(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
            );

    std::bind将接收到的可调用对象和参数封装构造出一个std::packaged_task<>对象,类型由上文语句用std::result_of<>推导。并且采用shared_ptr<>封装这个对象,实现内存管理。(注意这里使用智能指针封装的操作非常关键,下文会提到为什么。)

  4. std::future<return_type> res = task->get_future();将std::packaged_task<>对象的返回值交给std::future<>用于未来获取执行后获得的返回值。

  5. 接下来这句,是enqueue()函数的重中之重:

tasks.emplace([task]() { (*task)(); });
  •  为什么需要lambda表达式呢?

         此处使用lambda函数原因是1.便捷地捕获task. 2从而封装指针成一个可调用对象,不用关心返回值类型等问题。当然也可以通过函数实现这一功能,通过std::bind()捕获task对象与函数封装,函数中接收一个std::shared_ptr<std::packaged_task<return_type()> > 类型的参数,函数体中同样采用(*task)();实现调用。

  • 为什么一定要用智能指针shared_ptr<>来管理这个std::packged_task<>对象,我就想用裸指针行不行?

          当然可以,不过会存在一些些内存安全上的问题,下面给出例子:

template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type;

    auto task = new std::packaged_task<return_type()>(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
    ); // 裸指针封装

    std::future<return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex);

        // don't allow enqueueing after stopping the pool
        if (stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        tasks.emplace([task]() { (*task)(); delete task; });  // 使用裸指针
    }
    condition.notify_one();
    return res;
}

考虑以下情况:

  • 线程 A 向线程池中添加了一个任务。
  • 线程 B 从任务队列中获取任务并开始执行。
  • 在线程 B 执行任务时,线程 A 销毁了线程池。

在这个例子中,如果线程池在任务完成之前被销毁,任务的指针将成为悬空指针,从而导致未定义行为。此外,如果任务没有被正确执行,任务指针不会被释放,从而导致内存泄漏。(简单来说就是执行(*task)(); delete task; 这个代码块时,由于种种原因还没到delete task;这一句,因为析构函数中只关心你任务队列是不是空,stop是不是设置为true了,现成是不是结束了。可没有一个机制保证你任务对象一定会顺利完成。)

这时候又会有大聪明问了,我在析构函数中不是用了join()等待线程结束才析构,怎么不能保证任务对象顺利完成呢?哪有你说的这么严重?

        

        好好好,你先别急。join() 主要用于确保线程执行完毕后再执行析构函数,这样可以避免线程被销毁时出现悬空的线程句柄,从而避免资源泄漏。在ThreadPool中,join() 用于等待所有工作线程完成任务后再销毁线程池对象。但是!!!!

无法解决的问题

虽然 join() 确保了线程执行完毕,但并不能保证任务被正确执行完毕。如果任务因为异常或其他原因未能正确执行完毕,即使线程被 join(),任务的状态仍然是未完成的。也就是说我线程是执行完毕了,但是我的任务对象是异常了结束的。如果采用智能指针,我们就可以在任务执行过程中抛出了异常,并且在主线程中通过std::future::get() 捕获并处理了这个异常。在实际的应用中,你可以根据具体情况选择适合的异常处理策略,比如重试任务、记录日志等。也就是死也死得明明白白,而不是定位不到异常,找半天不知道Bug出哪了

       

通过使用 std::shared_ptr,可以确保任务对象的生命周期由智能指针自动管理,这样可以避免上述问题。即使线程池被销毁或者任务队列被清空,智能指针也能保证任务对象(智能指针的传递改变了任务对象的生命周期)在任务执行完毕后智能指针才会被正确销毁(通过引用计数),避免悬空指针和内存泄漏的问题。

(此处的std::packaged_task<>封装任务对象是不能省略的,因为我们需要异步地执行。 不采用std::function<>封装任务对象也是因为需要异步执行且通过std::future<>获取将来的执行结果!真是十万个为什么啊!)

  • 为什么一定要把这个指针(无论是智能指针还是裸指针)封装起来(通过函数封装或者lambda)?

        这里就是ThreadPool代码的精妙处之一了。假设我们不用指针,无论是智能不智能。我直接去把std::packaged_task<>放入队列(注意:std::packaged_task只支持移动,不支持复制,所以这里还得用std::move()将它放入队列),这样可行吗?

        先不管对不对,我们先假设它可以,那我们回到ThreadPool成员变量的声明中:

 // the task queue
    std::queue< std::function<void()> > tasks; // 原有的设计

 此时queue队列中的类型应该写成什么呢?注意,线程池接收的任务类型可以是多种多样的哦!成员函数、lambda、普通函数、函数对象。可是queue中存放的必须是类型相同的东东哦!那如果我硬要这么做呢,我不管我就不想封装!也行,那你可以写一个模板类XXX,这个模板类继承一个基类接口XX,基类中析构函数声明为虚函数然后bla bla bla ....。那在这里就可以写成:

 // the task queue
    std::queue<std::packaged_task<XX>> tasks; // 变!xx为基类接口

然而又回到上一个问题了,假如我在构造函数中通过std::move()获取到这个任务对象了,还没执行,不知道什么奇怪的原因,我析构函数来咯~然后ThreadPool对象结束生命周期,谁来保证我任务对象完成了!!!

        而通过lambda封装这个指针后,我不用关心返回值是啥玩意了。queue<>声明时候轻松了,因为std::function<void()>可以接收任意可调用的对象(包括lambda);同时智能指针的存在也保证了我这个任务对象一定会完成(智能指针没销毁,线程不会结束,析构也不会完成,就算执行出现异常我也能在主线程中捕获,并给出对应的措施)。

  • 好,我认可你的说法,但是我就想在队列中存入智能指针,然后在构造函数中调用。

        你TMD!行,我给你一种可能的实现方式:就是上面说的写一个基类接口然后bla bla bla,直接Show you the code好吧!

#pragma once
#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

// 基类接口
class ITask {
public:
    virtual ~ITask() {}; // 保证正确析构
    virtual void execute() = 0;
};

// 模板派生类
template<typename T>
class Task : public ITask {
public:
    Task(std::packaged_task<T()> task) : task_(std::move(task)) {}
    void execute() override {
        task_();
    }
private:
    std::packaged_task<T()> task_;
};

class ThreadPool {
public:
    ThreadPool(size_t);
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args)
        ->std::future<typename std::result_of<F(Args...)>::type>;
    ~ThreadPool();
private:
    // need to keep track of threads so we can join them
    std::vector<std::thread> workers;
    // the task queue
    std::queue<std::shared_ptr<ITask>> tasks; //  需要在queue中接收不同函数签名的任务,因此需要采用基类实现多态

    // synchronization
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
    : stop(false)
{
    for (size_t i = 0; i < threads; ++i)
        workers.emplace_back(
            [this]
            {
                for (;;) // threads个线程都在异步地无限循环接收任务队列中的任务
                {
                    std::shared_ptr<ITask> task;

                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock,
                            [this] { return this->stop || !this->tasks.empty(); });
                        if (this->stop && this->tasks.empty())
                            return;
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }

                    // 执行任务
                    task->execute();
                }
            }
            );
}

// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type;

    auto task = std::make_shared<std::packaged_task<return_type()> >(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );

    std::future<return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex);

        if (stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        // 将任务指针存入队列

        /*
        在C++中,模板类的实例化需要明确指定模板参数。Task是一个模板类,它需要一个模板参数来确定其类型。
        std::make_shared<Task<return_type>>中的<return_type>就是用于指定Task类的模板参数。这里的return_type
        依赖其他参数,编译器无法推导,需要显式指明
        */
        tasks.emplace(std::make_shared<Task<return_type>>(std::move(*task)));
    }
    condition.notify_one();
    return res;
}

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread& worker : workers)
        worker.join();
}

#endif

        

这下你应该没问题了吧,孩子!

  • 28
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值