当前位置: 首页
编程语言
从零实现一个轻量级C++线程池

从零实现一个轻量级C++线程池

热心网友 时间:2026-05-05
转载

一、引言

本文将带领你从零开始,动手实现一个轻量级、高性能且易于扩展的C++线程池。这不仅是一次深入理解多线程编程核心机制的绝佳实践,更能为你的C++项目带来显著的并发性能提升。

免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈

整个实现过程将系统性地运用以下现代C++并发编程关键技术:

  1. std::thread:实现工作线程的创建与生命周期管理。
  2. std::mutex / std::unique_lock:构建线程安全的共享数据保护机制,杜绝数据竞争。
  3. std::condition_variable:实现高效的线程间同步与任务等待通知模型。
  4. std::function / std::future / std::packaged_task / std::bind:构建灵活通用的任务封装、提交与异步结果获取体系。

二、什么是线程池

线程池是一种经典的并发设计模式,其核心思想在于线程的预创建与复用。它预先初始化一组工作线程并置于“池”中待命。当有任务需要异步执行时,无需临时创建线程,而是直接从池中分配一个空闲线程来执行任务,任务完成后线程回归池中等待下一次分配。

从零实现一个轻量级C++线程池

三、为什么需要线程池

频繁地动态创建和销毁线程会带来显著的系统开销,包括内核对象创建、上下文切换和内存分配等。C++线程池通过复用线程资源,有效解决了这一问题,并带来三大核心优势:

  • 显著降低系统开销:线程复用避免了反复创建/销毁的成本,提升了整体资源利用率和程序性能。
  • 提升任务响应速度:任务提交后立即可被池中空闲线程执行,省去了线程创建的时间延迟,尤其适合高并发、短任务的场景。
  • 增强线程的可控性与可管理性:线程作为系统稀缺资源,无限制创建易导致内存耗尽。线程池提供了统一的入口,允许开发者精确控制并发线程数量,便于进行性能调优、监控和资源隔离。

四、线程池的核心组成

一个健壮的C++线程池实现通常包含以下四个核心组件:

  1. 工作线程集合 (Worker Threads):线程池预先创建并维护的一组线程。它们持续运行,核心职责是从任务队列中获取并执行任务。
  2. 线程安全的任务队列 (Task Queue):作为生产者(任务提交者)与消费者(工作线程)之间的缓冲区。所有待处理的任务在此排队,必须保证其入队、出队操作的原子性与线程安全性。
  3. 同步与通信机制 (Synchronization)
    • 互斥锁 (Mutex):保护任务队列等共享数据结构,确保多线程并发访问时的数据一致性。
    • 条件变量 (Condition Variable):实现高效的任务等待/通知机制。当队列为空时,工作线程在此阻塞休眠;当新任务到达时,通知唤醒等待的线程,避免了CPU空转的忙等待(busy-waiting)。
  4. 通用任务提交接口 (Task Submission Interface):对外提供统一的任务提交API。它需要高度灵活,能够接收函数指针、成员函数、Lambda表达式、仿函数等任何可调用对象,并支持参数绑定与返回值获取。

五、C++线程池的完整实现与解析

在深入代码实现前,我们先简要回顾几个关键的C++标准库组件,它们是构建线程池的基石。

std::condition_variable(条件变量):线程同步的“信号灯”。工作线程在任务队列为空时,通过其wait()方法释放锁并进入阻塞状态。当submit()提交新任务后,调用notify_one()notify_all()唤醒等待的线程。这彻底消除了轮询带来的CPU资源浪费。

void wait (unique_lock& lck, Predicate pred);

该函数接受一个已锁定的互斥锁和一个谓词(Predicate)。内部会先释放锁,使线程阻塞,避免持有锁休眠。线程被唤醒后,会重新获取锁并检查谓词条件,只有谓词返回true才会真正继续执行,防止虚假唤醒。

void notify_one() noexcept;

随机唤醒一个在该条件变量上等待的线程。

void notify_all() noexcept;

唤醒所有在该条件变量上等待的线程。

std::future:用于获取异步任务结果的工具。它封装了异步操作的返回值或异常,提供了get()方法以同步等待方式获取结果,简化了线程间数据传递的复杂度。

T get();

阻塞调用线程,直至关联的异步任务完成,并返回其结果或抛出存储的异常。

std::function:通用的多态函数包装器。它将各种可调用实体(函数指针、Lambda、bind表达式、仿函数等)统一为特定签名的可调用对象,是构建通用任务队列类型的关键。

std::packaged_task:高级任务包装器。它将一个可调用对象与其异步结果(std::future)关联。通过get_future()获取future,执行任务后结果自动存入future,完美解决了任务返回值传递问题。

std::bind:通用参数绑定器。它可以将可调用对象与其部分或全部参数进行绑定,生成一个新的可调用实体,用于适配固定参数的任务提交接口。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

class ThreadPool {
public:
    ThreadPool(size_t thread_num = 4):
        _thread_num(thread_num),
        _start(false),
        _stop(false)
    {}
    ~ThreadPool(){
        if (_start && !_stop) stop();
    }
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;
    ThreadPool(ThreadPool&&) = delete;
    ThreadPool& operator=(ThreadPool&&) = delete;

    void start() {
        std::unique_lock lock(_mutex);
        if (_start) return;
        _workers.reserve(_thread_num);
        for (size_t i = 0; i < _thread_num; i++) {
            _workers.emplace_back(std::thread([this](){
                work_loop();
            }));
        }
        _start = true;
    }

    void stop() {
        {
            std::unique_lock lock(_mutex);
            if (!_start || _stop) return;
            // 在join回收线程之前,必须先将_stop置为true,
            // 否则工作线程可能会一直阻塞在条件变量上,导致无法正常退出,甚至会导致程序崩溃
            _stop = true;
        }
        _cond.notify_all();
        for (auto& worker : _workers) {
            if (worker.joinable()) worker.join();
        }
    }

    template
    auto submit(F&& f, Args&&... args)->std::future> {
        using return_type = std::invoke_result_t;
        // 绑定函数参数,并交给任务包装器
        auto task = std::make_shared>(
            std::bind(std::forward(f), std::forward(args)...)
        );
        // 获取关联的future
        std::future res = task->get_future();
        // 加锁+入队列
        {
            std::unique_lock lock(_mutex);
            if (_stop || !_start) throw std::runtime_error("线程池未启动!");
            _tasks.emplace([task](){(*task)();});
        }
        _cond.notify_one();
        return res;
    }

private:
    void work_loop() {
        while (true) {
            std::function task;
            {
                std::unique_lock lock(_mutex);
                _cond.wait(lock, [this](){
                    return _stop || !_tasks.empty();
                });
                if (_stop && _tasks.empty()) return;
                task = std::move(_tasks.front());
                _tasks.pop();
            }
            task();
        }
    }

private:
    std::vector _workers;
    std::queue> _tasks;
    std::mutex _mutex;
    std::condition_variable _cond;
    size_t _thread_num;
    bool _start;
    bool _stop;
};

int add(int a, int b) {
    return a + b;
}

void print() {
    std::cout << "-------------------print-------------------" << std::endl;
    std::cout << "Hello World!" << std::endl;
}

int main() {
    ThreadPool pool(4);
    pool.start();
    std::cout << "==================ThreadPoolTest==================" << std::endl;
    pool.submit([](){
        std::cout << "-------------------lambda-------------------" << std::endl;
        std::cout << "this is a lambda!" << std::endl;
    });
    std::this_thread::sleep_for(std::chrono::seconds(3));  
    auto ret1 = pool.submit(add, 10, 20);
    std::cout << "-------------------add-------------------" << std::endl;
    std::cout << "10 + 20 = " << ret1.get() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(3));  
    pool.submit(print);
    pool.stop();
    return 0;
}

运行结果:

从零实现一个轻量级C++线程池

本实现代码采用了C++17标准(使用了std::invoke_result_t等特性)。

一个关键设计在于:任务队列存储的是std::function类型,即无参数、无返回值的任务。那么,如何支持带参数和返回值的函数呢?这依赖于以下三步精妙的适配组合:

  1. 使用std::bind进行参数绑定:将目标函数与其实参预先绑定,生成一个符合return_type()签名的可调用对象。
  2. 使用std::packaged_task封装返回值:将上一步生成的可调用对象用packaged_task包装,其返回值会自动存入内部的std::future中。
  3. 使用Lambda进行最终类型擦除:通过一个捕获packaged_task智能指针的Lambda,调用其operator(),最终生成一个符合void()签名的任务,放入队列。

这套组合拳实现了强大的泛化能力,无论原函数签名如何,都能统一适配到线程池的任务队列中。

此外,代码中使用了std::forward进行完美转发,这确保了在submit模板函数中,传入的函数对象F和参数包Args...能够保持其原有的左值或右值引用类型,从而触发移动语义,避免不必要的拷贝,提升性能。

至此,一个具备生产级雏形的轻量级C++线程池就完整实现了。通过这个实践,你不仅能掌握线程池的核心原理,还能深入理解现代C++并发编程工具链的协同工作方式,为开发高性能并发应用打下坚实基础。

来源:https://www.jb51.net/program/36227121a.htm

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
Go语言中Struct Tag详解:XML解析必备的字段标签机制

Go语言中Struct Tag详解:XML解析必备的字段标签机制

Go语言Struct Tag深度解析:XML数据绑定与字段映射的核心机制 Struct Tag是Go语言为结构体字段附加元数据的核心语法,广泛应用于XML、JSON等数据序列化场景。它通过反引号包裹的键值对进行声明,本质上是指导编码器与解码器如何精确映射结构体字段与外部数据格式。缺少它,Go程序将无

时间:2026-05-05 22:54
c#如何调用Python脚本_c#Python脚本的最佳实践与常见坑点

c#如何调用Python脚本_c#Python脚本的最佳实践与常见坑点

C 调用Python脚本:最佳实践与常见坑点解析 使用 Process Start 调用 Python 脚本:最直接但需注意路径与环境 在大多数情况下,Process Start 是实现C 调用Python脚本最快捷的方案。它无需引入额外的NuGet包,也不强制要求Python解释器必须配置在系统环

时间:2026-05-05 22:53
c#如何定义常量_c#定义常量的3种方式

c#如何定义常量_c#定义常量的3种方式

C 常量定义:const、static readonly与静态类的实战指南 在C 编程实践中,常量的定义是基础但至关重要的环节。选择不当的常量声明方式,可能会为项目引入难以察觉的隐患。本文将深入解析C 中定义常量的三种核心方式:const、static readonly以及使用静态类进行封装,帮助你

时间:2026-05-05 22:53
c#如何使用MEF框架_c#MEF框架的正确用法与注意事项

c#如何使用MEF框架_c#MEF框架的正确用法与注意事项

CompositionContainer 初始化失败常因类型反射加载失败,主因是程序集版本 框架不匹配、DLL未显式加载或缺失部署依赖;Import为null则多因Catalog未包含对应Export、路径错误或契约不一致。 为什么 CompositionContainer 初始化失败常报“Unab

时间:2026-05-05 22:53
C#怎么压缩并解压ZIP文件_C#如何管理压缩包【实战】

C#怎么压缩并解压ZIP文件_C#如何管理压缩包【实战】

C 怎么压缩并解压ZIP文件_C 如何管理压缩包【实战】 说到在C 里处理ZIP文件,一个核心原则是:System IO Compression 是最稳妥的 ZIP 压缩方案。这意味着,你需要显式设置压缩级别为 CompressionLevel Optimal,使用正确的 ZipArchiveMod

时间:2026-05-05 22:53
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 日榜
  • 周榜
  • 月榜
热门教程
更多
  • 游戏攻略
  • 安卓教程
  • 苹果教程
  • 电脑教程