线程池学习(三) 实现固定线程池

一、fixedthreadpool.hpp

复制代码
#ifndef FIXEDTHREADPOOL_H
#define FIXEDTHREADPOOL_H

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


class fixedthreadpool {
public:
    fixedthreadpool(int thread_num);
    ~fixedthreadpool();

    // 支持任意参数和返回值,返还给小票
    template<typename f, typename... args>
    auto enqueue(f&& func, args&&... args_) -> std::future<decltype(func(args_...))> {

        using rettype = decltype(func(args_...));

        // 利用智能指针把任务打包成shared_ptr<packaged_task>,方便后续调用
        // 这里踩过坑:一开始用unique_ptr,后面发现无法传递,改成shared_ptr
        auto task = std::make_shared<std::packaged_task<rettype()>>(
            std::bind(std::forward<f>(func), std::forward<args>(args_)...)
        );

      //获得结果
        std::future<rettype> res = task->get_future();

        {
            std::lock_guard<std::mutex> lock(mtx);
            if (!is_running) {
                throw std::runtime_error("thread pool has stopped, can't enqueue task");
            }
            task_queue.push([task]() { (*task)(); });
        }

        cv.notify_one();

        return res;
    }

    void stop();

private:
    // 工作线程的主循环函数,每个线程都会执行这个函数
    void worker();

    std::vector<std::thread> threads;       // 存储所有工作线程
    std::queue<std::function<void()>> task_queue; // 任务队列
  matable  std::mutex mtx;                          // 保护任务队列的互斥锁
    std::condition_variable cv;              // 条件变量,用于线程等待/唤醒
    bool is_running;                         // 线程池运行状态标记
};

#endif

二、fixedthreadpool.cpp

几个关键踩坑点(重点记):

  1. worker函数中,必须用std::unique_lock而不是std::lock_guard,因为条件变量cv.wait()需要解锁,lock_guard不能手动解锁,unique_lock可以;

  2. cv.wait()的第二个参数是"唤醒条件",避免虚假唤醒------比如线程被唤醒后,发现队列还是空的,就继续等待;

  3. 停止线程池时,要先设置is_running为false,再唤醒所有线程,避免有线程还在等待,无法退出;

  4. 线程执行任务时,要加try-catch,避免单个任务抛出异常,导致整个线程崩溃;

  5. 析构函数中要判断is_running,如果线程池还在运行,就调用stop(),防止线程泄漏。

cpp 复制代码
#include "fixedthreadpool.hpp"
#include <iostream>

// 初始化线程池
fixedthreadpool::fixedthreadpool(int thread_num) {
    if (thread_num <= 0) {
        thread_num = 1;
    }
    is_running = true;
    // 启动指定数量的工作线程
    for (int i = 0; i < thread_num; ++i) {
        threads.emplace_back(&fixedthreadpool::worker, this);
    }
    // 调试用的cout,注释掉
    // std::cout << "fixed thread pool start, thread num: " << thread_num << std::endl;
}


fixedthreadpool::~fixedthreadpool() {
   
    if (is_running) {
        stop();
    }
}


void fixedthreadpool::worker() {
    // 循环取任务
    while (true) {
        // 包装一个任务
        std::function<void()> task;

        {
 
            std::unique_lock<std::mutex> lock(mtx);

            // 等待条件:队列不为空 或者 线程池停止
            // 这里踩过坑:一开始没写|| !is_running,导致线程池停止后,线程还在等待,无法退出
            cv.wait(lock, [this]() {
                return !task_queue.empty() || !is_running;
            });
            // 如果线程池停止,并且任务队列为空,就退出循环,结束
            if (!is_running && task_queue.empty()) {
                return;
            }
            // 从队列中取出任务,注意用std::move,避免拷贝,提高效率
            task = std::move(task_queue.front());
            task_queue.pop();
        }

        // 执行任务,加try-catch,防止任务异常导致线程崩溃
        try {
            task();
        } catch (const std::exception& e) {
            // 打印异常信息,方便调试
            std::cerr << "task execute error: " << e.what() << std::endl;
        }
    }
}

void fixedthreadpool::stop() {
    {
        // 加锁,避免多线程同时修改
        std::lock_guard<std::mutex> lock(mtx);
        is_running = false;
    }
    cv.notify_all();

    // 等待所有线程执行完毕,回收线程资源
    for (auto& t : threads) {
        if (t.joinable()) {
            t.join();
        }
    }
    // std::cout << "fixed thread pool stopped" << std::endl;
}

// 补充:这里可以加一个简单的测试函数,方便自己调试
// 我自己调试时写的,注释掉,需要测试时打开
/*
void test_task(int num) {
    std::cout << "task " << num << " execute, thread id: " << std::this_thread::get_id() << std::endl;
    // 模拟任务耗时
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

int main() {
    try {
        fixedthreadpool pool(4);
        // 提交10个任务,测试线程池是否正常工作
        for (int i = 0; i < 10; ++i) {
            pool.enqueue(test_task, i);
        }
        // 等待任务执行完毕
        std::this_thread::sleep_for(std::chrono::seconds(1));
        pool.stop();
    } catch (const std::exception& e) {
        std::cerr << "error: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}
*/

三、重点知识点讲解

1. 线程创建写法解释

cpp 复制代码
thread_group.emplace_back(&fixedthreadpool::worker,this);

很多新手疑惑,为什么不能直接只写 worker,必须加 &fixedthreadpool::

  1. worker 不是全局普通函数,它是类内部私有成员函数,单独写 worker 编译器找不到这个函数
  2. fixedthreadpool:: 是作用域限定符,明确告诉编译器这个 worker 属于 fixedthreadpool 这个类
  3. 前面的 & 作用是取出成员函数的内存地址,线程创建需要传入函数地址
  4. 末尾的 this 代表当前线程池对象,成员函数必须绑定对象才能调用内部成员变量

简单总结:全局函数直接写名字就行,类内成员函数创建线程必须带上 类名::加取地址符号

2. 锁的使用区别

  1. lock_guard:简单自动加锁解锁,无法手动解锁,只适合短时间锁定代码
  2. unique_lock:灵活锁,支持手动解锁、等待休眠,搭配条件变量 cv 必须用这个

3. 线程休眠唤醒逻辑

条件变量 wait 函数会自动释放互斥锁,线程进入休眠状态,等到有新任务提交调用 notify_one,就会唤醒一条空闲线程去执行任务,极大减少 cpu 空转消耗。

4. 停止逻辑

修改运行标记为关闭状态,唤醒所有休眠线程,让所有线程走完循环正常退出,最后调用 join 回收线程资源,不会出现内存泄漏。

四、简单测试代码

cpp 复制代码
#include "fixedthreadpool.hpp"
#include <chrono>
#include <iostream>

void testwork(int num)
{
    std::cout<<"执行任务编号:"<<num<<" 线程id:"<<std::this_thread::get_id()<<std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
}

int main()
{
    fixedthreadpool pool(3);
    for(int i = 1;i <= 8;i++)
    {
        pool.enqueue(testwork,i);
    }
    std::this_thread::sleep_for(std::chrono::seconds(2));
    pool.stop();
    return 0;
}

五、固定线程池优缺点总结

优点

  1. 逻辑简单易懂,上手最快,适合新手入门学习
  2. 线程数量固定,系统资源占用平稳,不会突然大量创建线程拖垮程序
  3. 代码量少,维护简单,稳定性高

缺点

  1. 线程数量固定无法动态调整,任务少时浪费线程资源
  2. 高并发大量任务堆积时,队列容易积压,处理速度变慢
  3. 不适合突发大量短时 io 任务场景

六、学习总结

写完固定线程池之后,彻底理清了线程池最核心的运行逻辑:线程复用、任务队列排队、互斥锁保证安全、条件变量实现休眠唤醒。

这也是后续学习缓存线程池、优先级线程池、工作窃取线程池的基础,弄懂这一套基础逻辑,后续其他线程池只需要改动任务队列规则、线程创建销毁规则即可。

相关推荐
橘子海全栈攻城狮1 小时前
【最新源码】基于springboot的快递物流平台的设计与实现C102
java·开发语言·spring boot·后端·spring·web安全
之歆1 小时前
DAY_24JavaScript 面向对象深度全解:Object、构造函数与 this 系统指南(上)
开发语言·前端·javascript·原型模式
KKei16381 小时前
Flutter for OpenHarmony学习小组组队与打卡APP技术文章
学习·flutter·华为·harmonyos
GHL2842710901 小时前
MinerU学习
学习·ai
nazisami1 小时前
初识AVL树
c++·面向对象·avl树
charlie1145141911 小时前
通用GUI编程技术——图形渲染实战(四十三)——D3D12设计哲学:显式控制与性能解锁
学习·3d·c·图形渲染·win32
sakiko_1 小时前
Swift报错合集(Xcode编译器)
开发语言·swiftui·xcode·swift·uikit
海盗12341 小时前
C#中使用MiniExcel 快速入门:读写 .xlsx 文件
开发语言·windows·c#
XMYX-01 小时前
29 - Go time 时间模块详解:时间处理、定时控制与底层设计
开发语言·golang