C++线程池

引言

线程池(Thread Pool)是一种并发编程技术,通过预先创建一组线程并重复利用它们来执行任务,从而避免频繁创建和销毁线程的开销。线程池的核心思想是任务与线程解耦:任务被提交到一个共享队列中,空闲线程从队列中获取任务并执行。(给大家30秒思考一下,如果是你,你会怎样实现?)

线程池典型工作流

1. 初始化:创建固定数量的线程,并使其进入等待状态。
2. 提交任务:用户将任务添加到任务队列中。
3. 任务分配:空闲线程从队列中获取任务并执行。
4. 线程复用:任务完成后,线程返回空闲状态,等待下一个任务。
5. 终止:当线程池关闭时,所有线程安全退出。

线程池核心组件

1. 线程集合:预先创建的一组工作线程,用于执行任务。
2. 任务队列:存储待处理的任务,支持按优先级或先进先出(FIFO)等策略调度。
3. 同步机制:通过互斥锁(mutex)和条件变量(condition_variable)确保线程安全。
4. 管理逻辑:控制线程的启动、任务分配、线程回收等。

优先级任务队列

观复君想要实现带有优先级的任务队列,在每次任务插入时,容器能够自动排序,确保优先级高的队列总是第一个被执行。C++中的优先级队列容器是一个好的选择。

template<
    class T,
    class Container = std::vector<T>,
    class Compare = std::less<typename Container::value_type>

> class priority_queue;

我先列出我的优先级队列code实现,下面会解释为什么这么做?

typedef std::function<void()> Task_type;
enum TaskPriority_E
{
    LOW,
    MIDDLE,
    HIGH
};
typedef std::pair<TaskPriority_E, Task_type> TaskPair;

struct cmp
{
    bool operator()(std::pair<TaskPriority_E, Task_type>& a, std::pair<TaskPriority_E, Task_type>& b)
    {
        return a.first < b.first;
    }
};

std::priority_queue<TaskPair, std::vector<TaskPair>, cmp> _tasks;

1. 模板参数解析

  • 元素类型 TaskPair
    TaskPair 是 std::pair<TaskPriority_E, Task_type> 的别名,其中:

    • TaskPriority_E:一个枚举类型,定义任务优先级(如 LOWMIDDLEHIGH)。

    • Task_typestd::function<void()>,表示一个无参数、无返回值的可调用对象(如函数、Lambda)。当然你也可以定义其他类型的function。通过将优先级与任务绑定,确保队列能按优先级调度任务。

  • 底层容器 std::vector<TaskPair>
    优先队列默认使用 std::vector 作为底层存储结构,原因包括:

    • 连续内存分配:支持快速随机访问,堆调整操作(如 sift-up 和 sift-down)更高效。

    • 动态扩容:自动管理内存,无需手动处理容量问题。

    • 性能优势:相比 std::dequestd::vector 在大多数场景下对堆操作更友好。

  • 比较函数 cmp
    cmp 是一个自定义的结构体(C++中结构体就相当于Class),通过重载 operator() 定义优先级比较规则:

    struct cmp {
        bool operator()(TaskPair& a, TaskPair& b) {
            return a.first < b.first; // 优先级高的任务排在前面
        }
    };
    • 最大堆逻辑:若 a.first < b.first,则 a 的优先级低于 bb 会被调整到堆顶。

    • 结果HIGH 优先级的任务会先于 MIDDLE 和 LOW 被取出执行。

2. 优先队列的特性

  • 自动排序
    每次插入新任务(push)或删除任务(pop)时,优先队列会自动调整内部结构,确保堆顶始终是优先级最高的任务。

    • 插入时间复杂度:O(log n)

    • 删除时间复杂度:O(log n)

  • 操作接口

    • push(const TaskPair& task):将任务插入队列,触发堆调整。

    • pop():移除堆顶任务(优先级最高),并调整堆结构。

    • top():返回堆顶任务的引用(不删除)。

    • empty():判断队列是否为空。

    • size():返回队列中任务数量。

任务的添加与获取 

  • 任务添加
    通过 addTask 方法将任务加入队列:

    void ThreadPool::addTask(const TaskPair& taskPair) {
        std::unique_lock<std::mutex> lock(_mutex);
        _tasks.emplace(taskPair); // 直接构造任务,避免拷贝
        _cond.notify_one();        // 唤醒等待线程
    }
    也可以实现一个重载函数,用户只需传入Task_type类型的任务(默认优先级为MID)即可。
    void ThreadPool::addTask(const Task_type& task)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        {
            TaskPair taskPair(MIDDLE, task);
            _tasks.emplace(taskPair);
            _isConditionMet = 1;
            _cond.notify_one();
        }
    }
    • emplace 直接在底层容器中构造任务,减少拷贝开销。

    • 添加任务后,通过条件变量 _cond 通知一个等待线程处理任务。

  • 任务取出
    通过 take() 方法从队列中取出优先级最高的任务:

    Task_type ThreadPool::take() {
        std::unique_lock<std::mutex> lock(_mutex);
        // 等待队列非空或线程池关闭
       _cond.wait(lock, [this]() { return (_isConditionMet == 1); });
        
        if (!_tasks.empty() && _started) {
            Task_type task = _tasks.top().second; // 获取任务函数
            _tasks.pop();                        // 移除任务
            return task;
        }
        return nullptr; // 若线程池关闭,返回空任务
    }
    • 使用 _tasks.top() 获取堆顶任务(优先级最高)。

    • _tasks.pop() 移除堆顶任务,触发堆调整。

线程同步与通信

1. 互斥锁(std::mutex

  • 作用:保护共享资源(任务队列 _tasks 和状态变量)的线程安全访问。

  • 关键代码段

    std::mutex _mutex;  // 声明互斥锁
    
    // 在 addTask 和 take 方法中,使用 unique_lock 加锁
    void ThreadPool::addTask(const TaskPair& taskPair) {
        std::unique_lock<std::mutex> lock(_mutex); // 自动加锁
        _tasks.emplace(taskPair);
        _cond.notify_one();  // 通知等待线程
    }
    
    Task_type ThreadPool::take() {
        std::unique_lock<std::mutex> lock(_mutex); // 自动加锁
        _cond.wait(lock, [this]() { return (_isConditionMet == 1); });
        // ... 操作队列
    }
  • 解析

    • std::unique_lock 在构造函数中自动加锁,析构时自动解锁,确保临界区代码的独占访问。

    • 所有对任务队列 _tasks 的操作(如 emplacetoppop)均在锁的保护下进行,避免多线程竞争。

2. 条件变量(std::condition_variable

  • 作用:实现线程间的通知机制,当任务队列状态变化时唤醒等待线程。

  • 关键代码段

    std::condition_variable _cond;  // 声明条件变量
    
    // 添加任务后通知等待线程
    void ThreadPool::addTask(const TaskPair& taskPair) {
        // ...
        _cond.notify_one();  // 唤醒一个等待线程
    }
    
    // 线程等待任务到达或线程池关闭
    Task_type ThreadPool::take()
    {
        std::unique_lock<std::mutex> lock(_mutex);
        while (_tasks.empty() && _started)
        {
            LOG(DEBUG) << "@_@ enter wait...";
            // _cond.wait(lock);
            _cond.wait(lock, [this]() { return (_isConditionMet == 1); });
            _isConditionMet = 0;
            LOG(DEBUG) << "@_@ leave wait...";
        }
    
        Task_type task;
        int size = _tasks.size();
        if (!_tasks.empty() && _started)
        {
            task = _tasks.top().second;
            _tasks.pop();
            assert((size - 1) == _tasks.size());
        }
        return task;
    }
  • 解析

    • _cond.wait(lock, predicate):线程释放锁并进入阻塞,直到其他线程调用 notify_one() 或 notify_all(),且 predicate 返回 true

    • 条件谓词的作用:防止虚假唤醒(Spurious Wakeup)。即使线程被唤醒,仍需检查队列是否非空或线程池是否已关闭。

    • notify_one() vs notify_all()

      • notify_one() 唤醒一个等待线程,适用于任务队列每次只添加一个任务的场景。

      • notify_all() 唤醒所有等待线程,适用于需要批量处理任务或关闭线程池的场景(例如析构函数中)。

3. 原子变量(std::atomic
  • 作用:实现无锁的线程安全操作,用于统计和状态标记。

  • 关键变量

    std::atomic<int> _num_of_valid_thread;  // 当前可用线程数
    std::atomic<uint8_t> _isConditionMet;   // 条件是否满足的标记
  • 解析

    • _num_of_valid_thread

      • 在 threadLoop 中,线程执行任务前递减,执行完成后递增。

      • 用于跟踪可用线程数,确保在析构时所有线程都能正确退出。

    • _isConditionMet

      • 在添加任务时设为 1,在 take() 中等待条件变量时检查该标记。

      • 替换方案:代码中 _cond.wait(lock, [this]() { return (_isConditionMet == 1); }) 的设计也可以通过直接检查任务队列状态(如 !_tasks.empty())来实现。

        _cond.wait(lock, [this]() { return !_tasks.empty() || !_started; });

设计优势

  • 优先级调度
    通过自定义比较函数 cmp,实现任务按优先级处理,确保关键任务(如 HIGH)优先执行。

  • 线程安全
    所有队列操作(pushpoptop)均在互斥锁 _mutex 保护下进行,避免多线程竞争。

  • 高效内存管理
    std::vector 作为底层容器,结合 emplace 方法,减少了任务对象的构造和拷贝开销。

完整实现

头文件

#ifndef _OTA_THREAD_POOL_HEADER_
#define _OTA_THREAD_POOL_HEADER_

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <string>
#include <utility>
#include <assert.h>
#include <vector>
#include <queue>
#include <unistd.h>
#include <atomic>
#include "ota_log.hpp"

typedef std::function<void()> Task_type;
enum TaskPriority_E
{
    LOW,
    MIDDLE,
    HIGH
};
typedef std::pair<TaskPriority_E, Task_type> TaskPair;

struct cmp
{
    bool operator()(std::pair<TaskPriority_E, Task_type>& a, std::pair<TaskPriority_E, Task_type>& b)
    {
        return a.first < b.first;
    }
};

class ThreadPool
{
public:
    ThreadPool(int size);
    ThreadPool(const ThreadPool&)                  = delete;
    const ThreadPool& operator=(const ThreadPool&) = delete;
    virtual ~ThreadPool();

    void start();
    void threadLoop();

    void addTask(const Task_type& task);
    void addTask(const TaskPair& taskPair);

    Task_type take();

private:
    void join();

private:
    int                                                       _thread_size;
    std::vector<std::thread*>                                 _threads;
    std::priority_queue<TaskPair, std::vector<TaskPair>, cmp> _tasks;
    std::mutex                                                _mutex;
    std::condition_variable                                   _cond;
    bool                                                      _started;
    OTA::LogOTA                                               mlog;
    std::atomic<int>                                          _num_of_valid_thread;
    std::atomic<uint8_t>                                      _isConditionMet;
};

#endif  //_OTA_THREAD_POOL_HEADER_

CPP代码

#include "ota_thread_pool.h"

ThreadPool::ThreadPool(int size) : _thread_size(size), _started(false), mlog("t-pool")
{
    _num_of_valid_thread = size;
    _isConditionMet = 0;
    start();
    LOG(INFO) << "ThreadPool constructor! Thread size is " << size;
}

ThreadPool::~ThreadPool()
{
    LOG(INFO) << "ThreadPool ready exit";
    _started = false;
    for (int i = 0; i < _thread_size; i++)
    {
        _isConditionMet = 1;
        _cond.notify_one();
        usleep(20 * 1000);
    }
    join();
    LOG(INFO) << "ThreadPool destructor!";
}

void ThreadPool::start()
{
    assert(_threads.empty());
    assert(!_started);
    _started = true;
    _threads.reserve(_thread_size);
    LOG(INFO) << "Create threadPool start";
    for (int i = 0; i < _thread_size; i++)
    {
        _threads.push_back(new std::thread(std::bind(&ThreadPool::threadLoop, this)));
        char name[32] = {0};
        sprintf(name, "thread_pool_%d", i);
        pthread_setname_np(_threads.at(i)->native_handle(), name);
    }
    LOG(INFO) << "Create threadPool end";
}

void ThreadPool::threadLoop()
{
    while (_started)
    {
        Task_type task = take();
        _num_of_valid_thread--;
        if (task)
        {
            LOG(DEBUG) << "@_@ exec job...";
            task();
            LOG(DEBUG) << "@_@ job done...";
        }
        _num_of_valid_thread++;
    }
}

void ThreadPool::addTask(const Task_type& task)
{
    std::unique_lock<std::mutex> lock(_mutex);
    {
        LOG(DEBUG) << "@_@ num_of_valid_thread - " << _num_of_valid_thread;
        TaskPair taskPair(MIDDLE, task);
        _tasks.emplace(taskPair);
        _isConditionMet = 1;
        _cond.notify_one();
    }
}

void ThreadPool::addTask(const TaskPair& taskPair)
{
    std::unique_lock<std::mutex> lock(_mutex);
    {
        LOG(DEBUG) << "@_@ num_of_valid_thread - " << _num_of_valid_thread;
        _tasks.emplace(taskPair);
        _isConditionMet = 1;
        _cond.notify_one();
    }
}

Task_type ThreadPool::take()
{
    std::unique_lock<std::mutex> lock(_mutex);
    while (_tasks.empty() && _started)
    {
        LOG(DEBUG) << "@_@ enter wait...";
        // _cond.wait(lock);
        _cond.wait(lock, [this]() { return (_isConditionMet == 1); });
        _isConditionMet = 0;
        LOG(DEBUG) << "@_@ leave wait...";
    }

    Task_type task;
    int size = _tasks.size();
    if (!_tasks.empty() && _started)
    {
        task = _tasks.top().second;
        _tasks.pop();
        assert((size - 1) == _tasks.size());
    }
    return task;
}

void ThreadPool::join()
{
    if (_threads.empty())
        return;
    for (int i = 0; i < _thread_size; i++)
    {
        _threads.at(i)->join();
    }
}

### C++ 线程池实现教程 #### 创建线程池类 为了创建一个简单的C++线程池,定义`ThreadPool`类来封装多个工作线程。该类负责管理一组预先启动的线程,并分配任务给这些线程执行。 ```cpp class ThreadPool { public: /// @brief Constructor that initializes the thread pool with a specified number of threads. explicit ThreadPool(size_t threads = std::thread::hardware_concurrency()) : stop(false) { if (threads == 0) { threads = 1; } for (size_t i = 0; i < threads; ++i) { workers.emplace_back( [this] { while (true) { 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 = std::move(this->tasks.front()); this->tasks.pop(); } task(); } } ); } } ~ThreadPool() { { std::unique_lock<std::mutex> lock(queue_mutex); stop = true; } condition.notify_all(); for (std::thread &worker: workers) worker.join(); } template<class F, class... Args> auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>; private: // Work queue and synchronization primitives std::vector<std::thread> workers; std::queue<std::function<void()>> tasks; std::mutex queue_mutex; std::condition_variable condition; bool stop; }; ``` 上述代码展示了如何通过构造函数初始化一定数量的工作线程[^4]。每个线程都在无限循环中等待新任务的到来;当接收到停止信号并且队列为空时,则退出循环并结束线程。 #### 添加任务到线程池 为了让外部能够向线程池提交新的任务,在`ThreadPool`类内部实现了模板成员函数`enqueue()`用于接收任意类型的可调用对象作为参数,并将其放入任务队列中等待被执行: ```cpp 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"); } tasks.push([task]() { (*task)(); }); } condition.notify_one(); return res; } ``` 此段代码说明了怎样安全地将任务加入到共享的任务队列里,并通知其中一个正在休眠中的工作者线程开始处理这个新到来的任务[^5]。 #### 测试线程池功能 最后提供了一个简单例子展示如何使用自定义的线程池来进行并发计算: ```cpp void* MyTaskFunc(void* arg) { int* i = static_cast<int*>(arg); printf("[MyTaskFunc]: thread[%lu] is working on %d\n", pthread_self(), *i); delete i; return nullptr; } int main(){ ThreadPool pool(10); // 初始化具有十个线程的线程池 for(int i=0;i<100;++i){ int* p=new int(i); pool.enqueue(MyTaskFunc,p); } return EXIT_SUCCESS; } ``` 这段程序片段演示了如何实例化一个含有固定数目线程的线程池,并连续不断地往里面塞入一百个小任务供其异步完成[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值