# 线程池详解:从原理到实现

本文整理自学习笔记,涵盖线程池概念、生产者-消费者模型、手撕线程池代码、双队列优化,以及线程数量计算公式。


一、什么是池式结构?

在聊线程池之前,先理解"池"这个概念。

池(Pool) 是一种经典的设计模式,核心思想是资源复用

池的类型 复用的是什么
内存池 动态内存分配
对象池 对象的创建与销毁
连接池 数据库 / 网络连接
请求池 HTTP 请求对象
线程池 线程的创建与销毁

线程为什么需要池?

  • 进程是操作系统分配资源的基本单位
  • 线程是操作系统进行 CPU 调度的基本单位
  • 线程参与 CPU 调度,不使用时进入休眠状态
  • 线程创建和销毁是有开销的------每次都 new/destroy 线程,系统负担很大

线程池的本质:预先创建一定数量的线程,用完不销毁,放回池中复用,避免频繁创建销毁带来的开销。


二、线程池解决什么问题?

核心场景

  1. 异步执行耗时任务------不让主线程(生产者线程)被耗时操作阻塞
  2. 充分利用多核------在多核 CPU 上并行处理任务

耗时有两种

类型 举例 特点
CPU 密集型 加密解密、图像处理、数值计算 持续占用 CPU,几乎不等待
IO 密集型 文件读写、网络请求、数据库查询 大部分时间在等待 IO 就绪

三、生产者-消费者模型

线程池的骨架是生产者-消费者模型

复制代码
┌─────────────────────────────────────────────┐
│              任务队列(ThreadPool)          │
│  ┌──────────────────────────────────────┐   │
│  │  task1 │ task2 │ task3 │ ... │ taskN │   │
│  └──────────────────────────────────────┘   │
│                                             │
│  生产者侧 (Producer)       消费者侧 (Consumer)│
│  ──────────────           ────────────────  │
│  主线程 / 工作线程           线程池中的 Worker  │
│  Push(task)                Pop() → 执行任务  │
└─────────────────────────────────────────────┘

流程:

  1. 生产者线程 将耗时任务 Push 到任务队列,然后继续干自己的事
  2. 消费者线程 (Worker)不断从队列 Pop 任务来执行
  3. 当队列为空,Worker 线程阻塞等待,不消耗 CPU
  4. 有新任务时,notify_one 唤醒一个休眠的 Worker

四、为什么用队列而不是栈?

队列(Queue) 栈(Stack)
结构 一端进、另一端出 一端进、同一端出
线程场景 生产者和消费者各占一端,职责清晰 生产者和消费者会抢同一端,容易冲突
时间复杂度 插入/删除都是 O(1) 插入/删除都是 O(1)

关键点:队列天然适配生产者-消费者模型------两端职责分离,不需要竞争同一个位置。


五、手撕线程池:基础版

5.1 阻塞队列 BlockingQueue

cpp 复制代码
#pragma once

#include <condition_variable>
#include <functional>
#include <queue>
#include <mutex>
#include <thread>

template<typename T>
class BlockingQueue {
public:
    explicit BlockingQueue(bool nonblock = false)
        : nonblock_(nonblock) {}

    // 入队
    void Push(const T& value) {
        std::lock_guard<std::mutex> lock(mutex_);  // RAII 自动释放锁
        queue_.push(value);
        not_empty_.notify_one();  // 唤醒一个等待中的线程
    }

    // 出队 ------ Pop 返回 true 表示成功,false 表示队列空或已取消
    bool Pop(T& value) {
        std::unique_lock<std::mutex> lock(mutex_);
        // wait 语义:
        //   1. unlock(mutex_)
        //   2. 线程在 wait 中阻塞,等待 notify
        //   3. 被唤醒后重新 lock(mutex_)
        //   4. 检查 lambda 条件:队列非空 或 已设置为 nonblock_
        not_empty_.wait(lock,
            [this] { return !queue_.empty() || nonblock_; });

        if (queue_.empty()) return false;  // nonblock_ 模式下直接返回

        value = queue_.front();
        queue_.pop();
        return true;
    }

    // 停止线程池时调用,解除所有阻塞在该队列上的线程
    void Cancel() {
        std::lock_guard<std::mutex> lock(mutex_);
        nonblock_ = true;
        not_empty_.notify_all();  // 唤醒全部等待线程
    }

private:
    bool nonblock_;                          // true = 非阻塞模式
    std::queue<T> queue_;                     // 任务队列
    std::mutex mutex_;                        // 保护队列的互斥锁
    std::condition_variable not_empty_;      // 队列非空条件变量
};

几个关键点:

  • std::lock_guard ------ RAII 思想,作用域结束自动解锁
  • std::unique_lock ------ 比 lock_guard 灵活,可以手动 unlock(wait 需要先解锁再阻塞)
  • wait + lambda ------ 解决 spurious wakeup(伪唤醒)问题
  • notify_one vs notify_all ------ 只唤醒一个等待线程,避免惊群效应

5.2 线程池 ThreadPool

cpp 复制代码
class ThreadPool {
public:
    // 禁止隐式转换:ThreadPool tp = 4; 不允许
    explicit ThreadPool(int threadNum) {
        for (int i = 0; i < threadNum; ++i) {
            workers_.emplace_back([this] { Worker(); });
        }
    }

    // 析构:先 Cancel 队列,再等待所有线程安全退出
    ~ThreadPool() {
        task_queue_.Cancel();
        for (auto& worker : workers_) {
            if (worker.joinable()) {
                worker.join();
            }
        }
    }

    // 提交任务
    void Post(std::function<void()> task) {
        task_queue_.Push(task);
    }

private:
    void Worker() {
        while (true) {
            std::function<void()> task;
            // Pop 返回 false = 线程池被 Cancel,Worker 退出
            if (!task_queue_.Pop(task)) {
                break;
            }
            task();  // 执行任务
        }
    }

    BlockingQueue<std::function<void()>> task_queue_;
    std::vector<std::thread> workers_;
};

使用示例:

cpp 复制代码
int main() {
    ThreadPool pool(4);  // 4 个 Worker 线程

    pool.Post([] { std::cout << "task 1\n"; });
    pool.Post([] { std::cout << "task 2\n"; });

    std::this_thread::sleep_for(std::chrono::seconds(1));
    // 析构时自动等待所有任务完成
    return 0;
}

六、手撕线程池:双队列优化

6.1 基础版的问题

在基础版中,所有 Worker 线程和所有生产者线程共用一把锁

  • 生产者 Push 时要抢锁
  • 消费者 Pop 时也要抢锁

当线程数量增多,锁竞争会成为性能瓶颈。

6.2 双队列思路

核心思想:交换队列,让生产者和消费者分别操作不同的队列,减少锁竞争。

复制代码
时刻 T1:
  生产者队列 (prod_queue_):  [taskA, taskB, taskC]
  消费者队列 (cons_queue_):   []

时刻 T2(SwapQueue_ 执行后):
  生产者队列 (prod_queue_):   []
  消费者队列 (cons_queue_):   [taskA, taskB, taskC]  ← Worker 从这里取

期间:
  - 生产者只操作 prod_queue_(持有 prod_mutex_)
  - 消费者只操作 cons_queue_(持有 cons_mutex_)
  - 两把锁完全独立,井水不犯河水

6.3 实现

cpp 复制代码
template<typename T>
class BlockingQueuePro {
public:
    explicit BlockingQueuePro(bool nonblock = false)
        : nonblock_(nonblock) {}

    // 生产者侧:只操作 prod_queue_
    void Push(const T& value) {
        std::lock_guard<std::mutex> lock(prod_mutex_);
        prod_queue_.push(value);
        not_empty_.notify_one();
    }

    // 消费者侧:只操作 cons_queue_
    bool Pop(T& value) {
        std::unique_lock<std::mutex> lock(cons_mutex_);
        if (cons_queue_.empty() && SwapQueue_() == 0) {
            return false;  // 无任务可处理
        }

        value = cons_queue_.front();
        cons_queue_.pop();
        return true;
    }

    void Cancel() {
        std::lock_guard<std::mutex> lock(prod_mutex_);
        nonblock_ = true;
        not_empty_.notify_all();
    }

private:
    // 原子地交换两个队列
    // 只有在 cons_queue_ 为空时才调用
    int SwapQueue_() {
        std::unique_lock<std::mutex> lock(prod_mutex_);
        not_empty_.wait(lock,
            [this] { return !prod_queue_.empty() || nonblock_; });

        std::swap(prod_queue_, cons_queue_);  // O(1) 交换指针
        return cons_queue_.size();
    }

    bool nonblock_;
    std::queue<T> prod_queue_;   // 生产者往这里写
    std::queue<T> cons_queue_;   // 消费者从这里读
    std::mutex prod_mutex_;      // 保护生产者队列
    std::mutex cons_mutex_;      // 保护消费者队列
    std::condition_variable not_empty_;
};

性能提升点:

  • 生产者和消费者操作不同的锁,可以并行执行
  • std::swap 是 O(1),只是交换指针,零拷贝
  • 大部分情况下,锁竞争大幅减少

七、线程数量怎么定?

线程数量和任务类型息息相关:

CPU 密集型

任务全是计算,不涉及 IO 等待。

经验公式:

复制代码
线程数 = CPU 核心数 + 1

多出来的 1 是为了当某个线程缺页中断时,其他线程还能干活。

IO 密集型

任务大部分时间在等待 IO 就绪,CPU 空闲。

公式:

复制代码
线程数 = CPU 核心数 × (CPU运算时间 + IO等待时间) / CPU运算时间

通常取 CPU 核心数的 2 倍,因为 IO 等待时间往往远大于 CPU 计算时间。

实际调优

理论值只是起点,真正的最优值需要实测

  1. 取不同的线程数(核心数、核心数+1、2倍、4倍...)
  2. 测量吞吐量(单位时间内完成的任务数)
  3. 画出曲线,找到拐点

八、完整代码汇总

cpp 复制代码
#pragma once

#include <condition_variable>
#include <functional>
#include <queue>
#include <mutex>
#include <thread>

// ==================== 基础阻塞队列 ====================
template<typename T>
class BlockingQueue {
public:
    explicit BlockingQueue(bool nonblock = false)
        : nonblock_(nonblock) {}

    void Push(const T& value) {
        std::lock_guard<std::mutex> lock(mutex_);
        queue_.push(value);
        not_empty_.notify_one();
    }

    bool Pop(T& value) {
        std::unique_lock<std::mutex> lock(mutex_);
        not_empty_.wait(lock,
            [this] { return !queue_.empty() || nonblock_; });
        if (queue_.empty()) return false;
        value = 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::queue<T> queue_;
    std::mutex mutex_;
    std::condition_variable not_empty_;
};

// ==================== 双队列优化版 ====================
template<typename T>
class BlockingQueuePro {
public:
    explicit BlockingQueuePro(bool nonblock = false)
        : nonblock_(nonblock) {}

    void Push(const T& value) {
        std::lock_guard<std::mutex> lock(prod_mutex_);
        prod_queue_.push(value);
        not_empty_.notify_one();
    }

    bool Pop(T& value) {
        std::unique_lock<std::mutex> lock(cons_mutex_);
        if (cons_queue_.empty() && SwapQueue_() == 0) {
            return false;
        }
        value = cons_queue_.front();
        cons_queue_.pop();
        return true;
    }

    void Cancel() {
        std::lock_guard<std::mutex> lock(prod_mutex_);
        nonblock_ = true;
        not_empty_.notify_all();
    }

private:
    int SwapQueue_() {
        std::unique_lock<std::mutex> lock(prod_mutex_);
        not_empty_.wait(lock,
            [this] { return !prod_queue_.empty() || nonblock_; });
        std::swap(prod_queue_, cons_queue_);
        return cons_queue_.size();
    }

    bool nonblock_;
    std::queue<T> prod_queue_;
    std::queue<T> cons_queue_;
    std::mutex prod_mutex_;
    std::mutex cons_mutex_;
    std::condition_variable not_empty_;
};

// ==================== 线程池 ====================
class ThreadPool {
public:
    explicit ThreadPool(int threadNum) {
        for (int i = 0; i < threadNum; ++i) {
            workers_.emplace_back([this] { Worker(); });
        }
    }

    ~ThreadPool() {
        task_queue_.Cancel();
        for (auto& worker : workers_) {
            if (worker.joinable()) {
                worker.join();
            }
        }
    }

    void Post(std::function<void()> task) {
        task_queue_.Push(task);
    }

private:
    void Worker() {
        while (true) {
            std::function<void()> task;
            if (!task_queue_.Pop(task)) {
                break;
            }
            task();
        }
    }

    // 换成 BlockingQueuePro 可获得更好的并发性能
    BlockingQueue<std::function<void()>> task_queue_;
    std::vector<std::thread> workers_;
};

九、总结

复制代码
┌──────────────────────────────────────────────────────────┐
│                      线程池核心要点                        │
├──────────────────────────────────────────────────────────┤
│  思想:资源复用 + 生产者-消费者模型                         │
│  阻塞队列:用条件变量实现线程间同步                         │
│  wait + lambda:防止伪唤醒                                │
│  双队列优化:生产者和消费者各用各的锁,减少竞争               │
│  线程数:CPU密集型→核心数+1,IO密集型→2×核心数              │
│  真正的最优值靠实测吞吐量确定                               │
└──────────────────────────────────────────────────────────┘
相关推荐
思麟呀2 小时前
HTTP的Cookie和Session
linux·网络·c++·网络协议·http
小明同学012 小时前
linux进程(下)
linux·服务器·c++
汉克老师2 小时前
GESP2023年12月认证C++三级( 第一部分选择题(1-8))
c++·string·字符数组·gesp三级·gesp3级
俺不要写代码2 小时前
lambda表达式理解
c++·算法
澈2072 小时前
动态内存管理:从基础到实战详解
c++·算法
想唱rap2 小时前
C++11之包装器
服务器·开发语言·c++·算法·ubuntu
wuminyu2 小时前
专家视角看Java的线程是如何run起来的过程
java·linux·c语言·jvm·c++
REDcker3 小时前
C++ std::move实现原理与vector扩容移动语义
开发语言·c++·c
脱氧核糖核酸__3 小时前
LeetCode热题100——48.旋转图像(题解+答案+要点)
c++·算法·leetcode