文章

C++ 并发编程实战:手写一个简易线程池 (ThreadPool)

深入解析 C++11 线程池的实现原理,通过代码实战讲解 std::thread, std::mutex, std::condition_variable 的协同工作机制,以及如何优雅地管理任务队列和线程生命周期。

C++ 并发编程实战:手写一个简易线程池 (ThreadPool)

在高性能服务器开发中,频繁地创建和销毁线程会带来巨大的系统开销。线程池 (Thread Pool) 技术应运而生,它通过复用一组固定的工作线程来执行大量短任务,极大地提高了系统的响应速度和资源利用率。

今天,我们将基于 C++11 标准库,从零实现一个简易但功能完备的线程池,并深入剖析其背后的并发同步机制。

🏗️ 核心设计思路

一个标准的线程池主要包含三个核心组件:

  1. 任务队列 (std::queue):存储待执行的任务(通常封装为 std::function)。
  2. 工作线程 (std::vector<std::thread>):一组常驻线程,不断从队列中取任务执行。
  3. 同步机制 (std::mutex + std::condition_variable)
    • 互斥锁:保护任务队列,防止多线程竞争。
    • 条件变量:当队列为空时,让工作线程进入休眠;当有新任务时,唤醒线程。

💻 完整代码实现

1. 头文件设计 (ThreadPool.h)

我们使用模板函数 submit 来支持任意可调用对象(Lambda、函数指针、bind 表达式等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#pragma once

#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <utility>
#include <unistd.h>
#include <thread>
#include <functional>
#include <iostream>

// 默认线程数
const int THREAD_POOL_SIZE = 4;

// 任务类型:无参无返回值的函数对象
using Job = std::function;

class ThreadPool {
private:
    std::vector workers;       // 工作线程池
    std::queue jobs;                   // 任务队列
    std::mutex m_mutex;                     // 互斥锁
    std::condition_variable m_condition;    // 条件变量
    bool stop;                              // 停止标志

public:
    ThreadPool(int pool_size = THREAD_POOL_SIZE);
    ~ThreadPool();

    // 提交任务 (模板函数,定义需在头文件中)
    template
    void submit(FUNC &&job);
};

// 模板函数的实现必须放在头文件中
template
void ThreadPool::submit(FUNC &&job) {
    {
        // 加锁保护队列
        std::lock_guard lock(m_mutex);
        // 完美转发任务到队列
        jobs.emplace(std::forward(job));
    }
    // 解锁后通知,减少锁竞争
    m_condition.notify_one();
}

2. 源文件实现 (ThreadPool.cpp)

这里是线程池的“心脏”,工作线程的生命周期管理在此完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include "ThreadPool.h"

ThreadPool::ThreadPool(int pool_size) : stop(false) {
    for (size_t i = 0; i < pool_size; i++) {
        workers.emplace_back([this]() {
            while (true) {
                Job job;
                {
                    // 获取独占锁
                    std::unique_lock lock(m_mutex);

                    // 等待条件:队列非空 或 收到停止信号
                    // wait 会自动释放锁并挂起线程,被唤醒后自动重新加锁
                    m_condition.wait(lock, [this]() {
                        return !this->jobs.empty() || this->stop;
                    });

                    // 如果收到停止信号且队列为空,则退出线程
                    if (stop && jobs.empty()) {
                        return;
                    }

                    // 取出任务
                    job = std::move(jobs.front());
                    jobs.pop();
                } 
                // 作用域结束,锁自动释放

                // 执行任务 (无锁状态,提高并发度)
                if (job) {
                    job();
                }
            }
        });
    }
}

ThreadPool::~ThreadPool() {
    {
        // 加锁修改停止标志
        std::lock_guard lock(m_mutex);
        stop = true;
    }
    
    // 唤醒所有正在等待的工作线程,让它们检查 stop 标志
    m_condition.notify_all();

    // 等待所有线程结束
    for (auto &worker : workers) {
        if (worker.joinable()) {
            worker.join();
        }
    }

    std::cout << "[ThreadPool] All worker threads stopped gracefully." << std::endl;
}

🔍 核心原理解析

1. 为什么使用 std::unique_lock 而不是 lock_guard

submit 函数中,我们只需要短暂的加锁入队,所以使用轻量级的 lock_guard。 但在工作线程的 while 循环中,我们需要在等待条件时自动释放锁std::condition_variable::wait() 要求传入一个 std::unique_lock,因为它需要在内部原子地执行“释放锁 + 挂起线程”的操作,避免竞态条件。

2. 条件变量的等待谓词 (Predicate)

1
2
3
m_condition.wait(lock, [this]() {
    return !this->jobs.empty() || this->stop;
});

这行代码等价于一个循环:

1
2
3
4
5
while (!(!this->jobs.empty() || this->stop)) {
    lock.unlock();
    wait_for_notification();
    lock.lock();
}

它确保了线程只有在“有任务”或者“需要停止”时才会醒来,有效避免了虚假唤醒 (Spurious Wakeup) 问题。

3. 优雅的退出机制

析构函数是线程池安全关闭的关键:

  1. 设置 stop = true
  2. 调用 notify_all() 唤醒所有因队列为空而休眠的线程。
  3. 工作线程醒来后,发现 stop 为真且队列为空,便会执行 return 退出 while 循环。
  4. 主线程调用 join() 等待所有工作线程清理完毕,确保资源不泄露。

🚀 测试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "ThreadPool.h"
#include <iostream>
#include <chrono>

int main() {
    ThreadPool pool(4); // 创建 4 个线程

    std::cout << "Submitting tasks..." << std::endl;

    // 提交 10 个任务
    for (int i = 0; i < 10; ++i) {
        pool.submit([i]() {
            std::cout << "Task " << i << " executed by thread " 
                      << std::this_thread::get_id() << std::endl;
            // 模拟耗时操作
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
        });
    }

    std::cout << "Tasks submitted. Waiting for completion..." << std::endl;
    
    // main 函数结束,pool 析构,自动等待所有任务完成
    return 0;
}

编译命令:

1
g++ -std=c++11 -pthread main.cpp ThreadPool.cpp -o thread_pool_demo

⚠️ 潜在优化与注意事项

虽然这个实现已经具备了线程池的核心功能,但在生产环境中还可以进一步优化:

  1. 任务返回值支持:当前 Job 定义为 void()。若要支持有返回值的任务,可以结合 std::packaged_taskstd::future,让 submit 返回 std::future<T>
  2. 动态扩缩容:当前线程数是固定的。可以根据 CPU 负载动态增加或减少工作线程。
  3. 异常处理:当前代码中,如果任务内部抛出异常且未捕获,会导致 std::terminate 终止整个程序。建议在 job() 执行外层包裹 try-catch 块。
  4. 无锁队列:对于极高并发场景,可以考虑使用无锁队列 (Lock-free Queue) 替代 std::queue + mutex 的组合,进一步减少锁竞争。

📝 总结

通过这个简易线程池的实现,我们深入理解了 C++ 并发编程的三大基石:线程管理互斥锁条件变量。掌握这些底层原理,不仅能帮助我们写出更高效的并发代码,也能让我们在使用现成库(如 Java Executor, Go Goroutine)时更加得心应手。

希望这篇博客能为你的 C++ 进阶之路提供一点帮助!欢迎在评论区交流讨论。


参考链接:

本文由作者按照 CC BY 4.0 进行授权