C++八股 —— 手撕线程池

article/2025/7/15 4:53:05

文章目录

    • 一、背景
    • 二、线程池实现
      • 1. 任务队列和工作线程
      • 2. 构造和析构函数
      • 3. 添加任务函数
      • 4. 完整代码
    • 三、阻塞队列实现
      • 1. 基础队列
      • 2. 升级版队列
    • 四、测试代码
    • 五、相关问题
    • 六、其他实现方式

来自:华为C++一面:手撕线程池_哔哩哔哩_bilibili

华为海思

手撕线程池

相关概念参考

  • C++八股——函数对象、Lambda、bind、function_c++八股文-CSDN博客
  • C++ 11 lock_guard 和 unique_lock_lockguard-CSDN博客
  • explicit关键字
  • C++ —— 可变参数_c++ 可变参数-CSDN博客
  • 阻塞队列(超详细易懂)-CSDN博客

一、背景

  1. 什么是线程池

    维持管理一定数量线程的池式结构。

    核心思想:线程复用。 避免频繁地创建和销毁线程带来的开销。

  2. 为什么需要线程池

    • 创建/销毁线程的开销大,线程池可以有效降低资源消耗、提高响应速度
    • 提高线程的可管理性
    • 防止因任务过多导致无限制创建线程而耗尽系统资源的问题。
  3. 线程池的工作流程

    核心为生产者-消费者模型

    image-20250530164219270

    线程池需要维护工作线程(消费者线程)和一个任务队列生产者线程创建任务放入线程池的任务队列,消费者线程从任务队列中取出任务执行。

二、线程池实现

一个线程池包含:

  • 任务队列:存放生产者线程创建的任务
  • 工作线程:取出任务队列中任务执行
  • 构造函数
  • 析构函数
  • 添加任务函数
  • 工作线程函数

1. 任务队列和工作线程

任务队列使用一个手动实现的阻塞队列来实现;

工作线程使用一个线程vector来实现。

BlockingQueuePro<std::function<void()>> task_queue_; // 任务队列
std::vector<std::thread> workers_; // 工作线程列表

工作线程函数是一个不断循环的函数,从任务队列中取出任务并执行

// 工作线程函数
void Worker() {while (true) {std::function<void()> task;if (!task_queue_.Pop(task))break;task(); // 执行任务}
}

2. 构造和析构函数

构造函数传入一个整数作为线程池最大线程数,然后创建该数量的线程

// 构造函数
explicit ThreadPool(int num_threads) {for (size_t i = 0; i < num_threads; i++) {workers_.emplace_back([this] { Worker(); });}
}

析构函数将阻塞队列设置为非阻塞模型,并阻塞当前线程等待所有工作线程执行完毕

// 析构函数
~ThreadPool() {task_queue_.Cancel();for (auto &worker : workers_) {if (worker.joinable()) {worker.join();}}
}

3. 添加任务函数

Post函数传入一个可调用对象和参数,将可调用对象和参数绑定之后加入到工作队列中。

// 添加任务
template <typename F, typename... Args>
void Post(F &&f, Args &&...args) {auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);task_queue_.Push(task);
}

4. 完整代码

class ThreadPool {
public:// 构造函数explicit ThreadPool(int num_threads) {for (size_t i = 0; i < num_threads; i++) {workers_.emplace_back([this] { Worker(); });}}// 析构函数~ThreadPool() {task_queue_.Cancel();for (auto &worker : workers_) {if (worker.joinable()) {worker.join();}}}// 添加任务template <typename F, typename... Args>void Post(F &&f, Args &&...args) {auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);task_queue_.Push(task);}private:// 工作线程函数void Worker() {while (true) {std::function<void()> task;if (!task_queue_.Pop(task))break;task(); // 执行任务}}BlockingQueuePro<std::function<void()>> task_queue_; // 任务队列std::vector<std::thread> workers_; // 工作线程列表
};

三、阻塞队列实现

阻塞队列是一种特殊的队列,同样遵循“先进先出”的原则,支持入队操作和出队操作。在此基础上,阻塞队列会在队列已满或队列为空时陷入阻塞,使其成为一个线程安全的数据结构,它具有如下特性:

  • 当队列已满时,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
  • 当队列为空时,继续出队列也会阻塞,直到有其他线程向队列中插入元素。

(引用参考:阻塞队列(超详细易懂)-CSDN博客)

1. 基础队列

生产者和消费者共用一个队列互斥锁

  • 当队列为空时,使工作线程进入休眠。
  • 当队列被设置为非阻塞时,队列任务为空会使工作线程结束

源码

template <typename T>
class BlockingQueue {
public:BlockingQueue(bool nonblock = false) : nonblock_(nonblock) {}// 添加任务void Push(const T &task) {std::lock_guard<std::mutex> lock(mutex_);queue_.push(task);not_empty_.notify_one(); // 通知一个等待的线程}// 获取任务bool Pop(T &task) {std::unique_lock<std::mutex> lock(mutex_);not_empty_.wait(lock, [this] { return !queue_.empty() || nonblock_; });if (queue_.empty()) return false; task = queue_.front();queue_.pop();return true;}// 解除阻塞当前队列的线程void Cancel() {std::lock_guard<std::mutex> lock(mutex_);nonblock_ = true; // 设置为非阻塞状态not_empty_.notify_all(); // 通知所有等待的线程}private:bool nonblock_; // 是否为非阻塞模式std::mutex mutex_; // 互斥锁std::condition_variable not_empty_; // 条件变量,队列为空时线程休眠std::queue<T> queue_; // 任务队列
};

2. 升级版队列

生产者和消费者有各自的任务队列和互斥锁

  • 当消费者队列为空时,会尝试与生产者队列交换

    • 若交换中生产者队列为空,使工作线程进入休眠;

    • 若队列被设置为非阻塞,生产者队列为空,交换后消费者队列仍为空,此时会结束工作线程。

源码

// 升级版队列,多生产者和多消费者
template <typename T>
class BlockingQueuePro {
public:BlockingQueuePro(bool nonblock = false) : nonblock_(nonblock) {}// 添加任务void Push(const T &task) {std::lock_guard<std::mutex> lock(producer_mutex_);producer_queue_.push(task);not_empty_.notify_one(); // 通知一个等待的线程}// 获取任务bool Pop(T &task) {std::unique_lock<std::mutex> lock(consumer_mutex_);// 如果消费者队列为空,尝试交换生产者队列if (consumer_queue_.empty() && SwapQueue_() == 0) {return false; // 如果交换后仍然为空,则返回false}task = consumer_queue_.front();consumer_queue_.pop();return true;}// 解除阻塞当前队列的线程void Cancel() {std::lock_guard<std::mutex> lock(producer_mutex_);nonblock_ = true; // 设置为非阻塞状态not_empty_.notify_all(); // 通知所有等待的线程}private:// 交换生产者队列到消费者队列size_t SwapQueue_() {std::unique_lock<std::mutex> lock(producer_mutex_);not_empty_.wait(lock, [this] { return !producer_queue_.empty() || nonblock_; });std::swap(producer_queue_, consumer_queue_); // 交换队列return consumer_queue_.size(); // 返回新的消费者队列大小}bool nonblock_; // 是否为非阻塞模式std::mutex producer_mutex_; // 生产者互斥锁std::mutex consumer_mutex_; // 消费者互斥锁std::condition_variable not_empty_; // 条件变量,队列为空时线程休眠std::queue<T> producer_queue_; // 生产者任务队列std::queue<T> consumer_queue_; // 消费者任务队列
};

四、测试代码

  • 任务函数Task():线程池中工作线程需要执行的任务
  • 生产者函数Producer():将num_tasks个任务添加到线程池中
  • 生产者线程producers:包含多个生产者,同时并行生成任务到线程池中
  • 等待生产者线程完成任务生成
  • 等待线程池执行完所有生成的任务
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
#include <chrono>#include "threadpool.h"// 全局计数器,统计任务完成的数量
std::atomic<int> task_counter(0);// 任务函数
void Task(int id) {std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟任务处理时间std::cout << "Task " << id << " executed by thread " << std::this_thread::get_id() << std::endl;task_counter++;
}// 生产者函数
void Producer(ThreadPool &pool, int producer_id, int num_tasks) {for (int i = 0; i < num_tasks; i++) {int task_id = producer_id * 1000 + i; // 生成唯一任务IDpool.Post(Task, task_id);std::cout << "Producer " << producer_id << " posted task " << task_id << std::endl;}
}int main() {const int num_producers = 3; // 生产者数量const int tasks_per_producer = 5; // 每个生产者生成的任务数量const int num_threads = 4; // 线程池中的线程数量ThreadPool pool(num_threads); // 创建线程池// 启动多个生产者线程std::vector<std::thread> producers;for (int i = 0; i < num_producers; i++) {producers.emplace_back(Producer, std::ref(pool), i, tasks_per_producer);}// 等待所有生产者完成for (auto &producer : producers) {producer.join();}// 等待一段时间以确保所有任务都被处理完while (task_counter < num_producers * tasks_per_producer) {std::this_thread::sleep_for(std::chrono::milliseconds(100));}std::cout << "Total tasks executed: " << task_counter.load() << std::endl;return 0;
}

五、相关问题

  1. 条件变量与线程同步问题

    C++ 条件变量:wait、wait_for、wait_until_c++ 条件变量 wait-CSDN博客

  2. 虚假唤醒问题

    • 一般为操作系统层面的原因导致的

      • 实现优化
        操作系统或条件变量的底层实现(如 Linux 的 futex)为了提高性能,允许在未收到信号时唤醒线程。例如:
        • 内核可能在处理信号时意外唤醒线程。
        • 多核 CPU 竞争资源时,硬件层面的竞争可能导致唤醒。
      • 设计妥协
        允许虚假唤醒可以简化条件变量的实现,同时减少某些场景下的唤醒延迟。
    • 解决方法

      // 循环检查
      while (condition) {cond.wait(lock);
      }
      // 谓词
      cond.wait(lock, [](return ready));
      
  3. 引用包装

    【C++】引用包装(std::ref与std::cref)-CSDN博客

六、其他实现方式

progschj/ThreadPool: A simple C++11 Thread Pool implementation

配合以下内容食用:

C++知识点记录-CSDN博客


http://www.hkcw.cn/article/ArldUhXiyY.shtml

相关文章

半导体厂房设计建造流程、方案和技术要点-江苏泊苏系统集成有限公司

半导体厂房设计建造流程、方案和技术要点-江苏泊苏系统集成有限公司 半导体厂房的设计建造是一项高度复杂、专业性极强的系统工程&#xff0c;涉及洁净室、微振动控制、电磁屏蔽、特殊气体/化学品管理等关键技术。 一、设计建造流程&#xff1a; 1.需求定义与可行性分析 &a…

gitLab 切换中文模式

点击【头像】--选择settings 选择【language】,选择中文&#xff0c;点击【保存】即可。

Redis 常用数据结构详解与实战应用

在当今互联网高速发展的时代&#xff0c;数据的存储和处理效率至关重要。Redis 作为一款高性能的内存数据库&#xff0c;凭借其丰富的数据结构和出色的性能&#xff0c;成为了众多开发者的首选。本文将深入探讨 Redis 常用的数据结构&#xff0c;并结合实际应用场景&#xff0c…

leetcode2221. 数组的三角和-medium

1 题目&#xff1a;数组的三角和 官方标定难度&#xff1a;中 给你一个下标从 0 开始的整数数组 nums &#xff0c;其中 nums[i] 是 0 到 9 之间&#xff08;两者都包含&#xff09;的一个数字。 nums 的 三角和 是执行以下操作以后最后剩下元素的值&#xff1a; nums 初始…

PPIO × AstrBot:多平台接入聊天机器人,开启高效协同 | 教程

在消息平台接入专属聊天机器人&#xff0c;能快速生成精准答案&#xff0c;与项目管理、CRM等系统集成后&#xff0c;机器人还能根据任务进展自动建群、推送进度提醒&#xff0c;并精准相关人员&#xff0c;实现信息的高效传递。 AstrBot 是一个多平台聊天机器人及开发框架&…

江科大SPI串行外设接口hal库实现

hal库相关函数 初始化结构体 typedef struct {uint32_t Mode; /*SPI模式*/uint32_t Direction; /*SPI方向*/uint32_t DataSize; /*数据大小*/uint32_t CLKPolarity; /*时钟默认极性控制CPOL*/uint32_t CLKPhase; /*…

【笔记】Suna 部署之获取 OpenAI API key

#工作记录 API Platform | OpenAI 一、注册或登录 OpenAI 账号 访问 OpenAI 官方网站&#xff08;platform.openai.com &#xff09;。若已有 ChatGPT 账号&#xff0c;可直接使用该账号登录。若无账号&#xff0c;点击注册&#xff08;Sign Up&#xff09;&#xff0c;填写有…

Java八股文——Java基础「概念篇」

参考小林Coding和Java Guide 说一下Java的特点 平台无关性&#xff1a;“Write Once, Run Anywhere”其最大的特点之一。Java编译器将源代码编译成字节码&#xff0c;该字节码可以在任何安装了JVM的系统上运行。面向对象&#xff1a;Java是一门严格的面向对象编程语言&#xf…

NHANES指标推荐:CQI

文章题目&#xff1a;The impact of carbohydrate quality index on menopausal symptoms and quality of life in postmenopausal women 中文标题&#xff1a;碳水化合物质量指数对绝经后妇女更年期症状和生活质量的影响 发表杂志&#xff1a;BMC Womens Health 影响因子&…

91.评论日记

2025年5月30日20:27:06 AI画减速器图纸&#xff1f; 呜呜为什么读到机械博士毕业了才有啊 | 新迪数字2025新品发布会 | AI工业软件 | 三维CAD | 国产自主_哔哩哔哩_bilibili

循环神经网络(RNN)全面教程:从原理到实践

循环神经网络(RNN)全面教程&#xff1a;从原理到实践 引言 循环神经网络(Recurrent Neural Network, RNN)是处理序列数据的经典神经网络架构&#xff0c;在自然语言处理、语音识别、时间序列预测等领域有着广泛应用。本文将系统介绍RNN的核心概念、常见变体、实现方法以及实际…

OrCAD X Capture CIS 设计小诀窍第二季 | 10. 如何自动将 270° 放置的网络名称修正为 90°

背景介绍&#xff1a;我们在进行原理图设计时&#xff0c;经常需要统一原理图的格式&#xff0c;从而保证原理图的美观和统一。而通过把所有270放置的网络名称修正为90可以避免因网络名称放置的方向不一致而造成混淆&#xff0c;比如6和9。但如果依靠设计师手动进行修改&#x…

核心机制:确认应答和超时重传

核心机制一:确认应答 实现让发送方知道接受方是否收到数据 发送方发送了数据之后,接受方,一旦接收到了,就会给发送方返回一个"应答报文"告诉发送方"我已经收到了数据" 网络上会出现"后发先至"的情况 为了解决上述问题,就引入了"序号和确…

特朗普:仍希望有国际学生在美国学习

当地时间5月30日,美国总统特朗普在白宫表示,仍希望有国际学生在美国学习。据美国政治新闻网站“Politico”27日的报道,特朗普政府已暂停新的学生签证面谈,同时考虑扩大对国际学生社交媒体审查范围。此外,据路透社30日援引一份美国国务卿发送给所有美国外交和领事馆的电报称…

两阶段uplift建模(因果估计+预算分配)的讲座与自己动手实践(一)

来自分享嘉宾在datafun论坛的分享&#xff0c;孙泽旭 中国人民大学高瓴人工智能学院 博士生分享的【面向在线营销场景的高效 Uplift 方法】 听讲座听的云里雾里&#xff0c;自己做点力所能及的小实践… 关于uplift笔者之前的博客&#xff1a; 因果推断笔记——uplift建模、met…

2025年通用 Linux 服务器操作系统该如何选择?

2025年通用 Linux 服务器操作系统该如何选择&#xff1f; 服务器操作系统的选择对一个企业IT和云服务影响很大&#xff0c;主推的操作系统在后期更换的成本很高&#xff0c;而且也有很大的迁移风险&#xff0c;所以企业在选择服务器操作系统时要尤为重视。 之前最流行的服务器…

Ubuntu20.04服务器开启路由转发让局域网内其他电脑通过该服务器连接外网

要让你的 Ubuntu作为路由器&#xff0c;通过 Wi-Fi 上网&#xff0c;并给连接到 UsbNetwork 的设备提供网络&#xff0c;需要做以下配置&#xff1a; 1. 网络拓扑 [互联网] ← (Wi-Fi, wlo1) → [Ubuntu] ← (USB网络/USB以太网, UsbNetwork) → [设备]Ubuntu&#xff1a; Wi-…

ONLYOFFICE深度解锁系列.4-OnlyOffice客户端原理-真的不支持多端同步

最近很多客户多要求直接部署onlyoffice服务端,还问能否和onlyoffice的客户端进行文件同步,当时真是一脸懵,还有的是老客户,已经安装了onlyoffice协作空间的,也在问如何配置客户端和协作空间的对接。由于问的人太多了,这里统一回复,先说结论,再说原理: 1.onlyoffice document s…

手撕Java+硅基流动实现MCP服务器教程

手撕Java硅基流动实现MCP服务器教程 一、MCP协议核心概念 MCP是什么 MCP 是 Anthropic (Claude) 主导发布的一个开放的、通用的、有共识的协议标准。 ● MCP 是一个标准协议&#xff0c;就像给 AI 大模型装了一个 “万能接口”&#xff0c;让 AI 模型能够与不同的数据源和工…

BG22L和BG24L精简版蓝牙SoC推动智能物联网走向更广天地

作者&#xff1a;Aashish Chaddha&#xff0c;芯科科技无线产品营销经理 随着物联网&#xff08;IoT&#xff09;领域的复杂性和互联性不断提高&#xff0c;对无线设备的需求正在发生变化。它不再只是将数据从A点传输到B点&#xff0c;现在的设备需要更智能、更节能&#xff0…