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

文章目录

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

华为海思

手撕线程池

相关概念参考

一、背景

  1. 什么是线程池

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

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

  2. 为什么需要线程池

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

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

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

二、线程池实现

一个线程池包含:

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

1. 任务队列和工作线程

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

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

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

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

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

2. 构造和析构函数

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

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

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

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

3. 添加任务函数

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

cpp 复制代码
// 添加任务
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. 完整代码

cpp 复制代码
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. 基础队列

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

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

源码

cpp 复制代码
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. 升级版队列

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

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

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

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

源码

cpp 复制代码
// 升级版队列,多生产者和多消费者
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:包含多个生产者,同时并行生成任务到线程池中
  • 等待生产者线程完成任务生成
  • 等待线程池执行完所有生成的任务
cpp 复制代码
#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; // 生成唯一任务ID
        pool.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 竞争资源时,硬件层面的竞争可能导致唤醒。
      • 设计妥协
        允许虚假唤醒可以简化条件变量的实现,同时减少某些场景下的唤醒延迟。
    • 解决方法

      cpp 复制代码
      // 循环检查
      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博客

相关推荐
kingmax542120083 小时前
【洛谷P9303题解】AC- [CCC 2023 J5] CCC Word Hunt
数据结构·c++·算法·广度优先
AgilityBaby5 小时前
UE5打包项目设置Project Settings(打包widows exe安装包)
c++·3d·ue5·游戏引擎·unreal engine
让我们一起加油好吗7 小时前
【基础算法】高精度(加、减、乘、除)
c++·算法·高精度·洛谷
鑫鑫向栄8 小时前
[蓝桥杯]缩位求和
数据结构·c++·算法·职场和发展·蓝桥杯
stormsha8 小时前
MCP架构全解析:从核心原理到企业级实践
服务器·c++·架构
梁下轻语的秋缘8 小时前
每日c/c++题 备战蓝桥杯(P1204 [USACO1.2] 挤牛奶 Milking Cows)
c语言·c++·蓝桥杯
鑫鑫向栄8 小时前
[蓝桥杯]外卖店优先级
数据结构·c++·算法·职场和发展·蓝桥杯
Zfox_8 小时前
【C++项目】:仿 muduo 库 One-Thread-One-Loop 式并发服务器
linux·服务器·c++·muduo库
wangyuxuan10299 小时前
AtCoder Beginner Contest 399题目翻译
开发语言·c++·算法
?!71410 小时前
Socket网络编程之UDP套件字
linux·网络·c++·网络协议·udp·php