C++ 笔记 高级线程同步原语与线程池实现

std::thread 基础上,C++11 还提供了 std::condition_variable(条件变量)std::atomic(原子变量) 两大高级同步原语,分别解决 "线程间协作通知" 和 "无锁数据竞争" 问题;而 线程池 则是对 std::thread 的高层封装,通过预创建线程池避免频繁创建 / 销毁线程的开销,是多线程开发的核心工具。本文将从底层原理、代码示例到完整实现,系统拆解这三大知识点。


一、std::condition_variable:线程间的条件同步

1.1 核心作用与原理

std::condition_variable 用于线程间的协作通知 :一个线程等待某个条件成立而阻塞,另一个线程在条件成立时通知阻塞线程继续执行。它必须与 std::unique_lock<std::mutex> 配合使用 ------mutex 保护共享条件,条件变量负责线程的阻塞与唤醒,二者结合解决 "忙等待(Busy Wait)" 问题(避免线程空转浪费 CPU)。

1.2 关键函数

表格

函数 作用
wait(lock) 阻塞当前线程,直到被通知;自动释放锁,被唤醒后重新获取锁
wait(lock, pred) 带谓词的 wait:阻塞直到被通知且 pred()true(自动处理虚假唤醒)
notify_one() 唤醒一个等待的线程
notify_all() 唤醒所有等待的线程

1.3 经典示例:生产者 - 消费者模型

这是条件变量最典型的应用场景:生产者线程生成数据放入队列,消费者线程从队列取数据处理,队列满时生产者阻塞,队列空时消费者阻塞。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>

std::queue<int> data_queue;       // 共享数据队列
std::mutex mtx;                    // 保护队列的互斥锁
std::condition_variable cv;        // 条件变量
const int MAX_QUEUE_SIZE = 5;     // 队列最大容量

// 生产者线程:生成数据放入队列
void producer() {
    for (int i = 1; i <= 10; ++i) {
        {
            std::unique_lock<std::mutex> lock(mtx);
            // 队列满时阻塞,等待消费者取数据(谓词防止虚假唤醒)
            cv.wait(lock, []() { return data_queue.size() < MAX_QUEUE_SIZE; });
            
            data_queue.push(i);
            std::cout << "[生产者] 放入数据: " << i << ",队列大小: " << data_queue.size() << "\n";
        }
        cv.notify_one(); // 通知一个消费者线程
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时
    }
}

// 消费者线程:从队列取数据处理
void consumer(int id) {
    while (true) {
        int data;
        {
            std::unique_lock<std::mutex> lock(mtx);
            // 队列空时阻塞,等待生产者放数据;同时检查是否生产结束
            cv.wait(lock, []() { return !data_queue.empty(); });
            
            data = data_queue.front();
            data_queue.pop();
            std::cout << "[消费者 " << id << "] 取出数据: " << data << ",队列大小: " << data_queue.size() << "\n";
        }
        cv.notify_one(); // 通知一个生产者线程
        std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 模拟消费耗时
        
        if (data == 10) break; // 生产结束,退出循环
    }
}

int main() {
    std::thread prod(producer);
    std::thread cons1(consumer, 1);
    std::thread cons2(consumer, 2);

    prod.join();
    cons1.join();
    cons2.join();

    return 0;
}

1.4 关键细节:虚假唤醒

wait() 可能在没有被 notify_one()/notify_all() 调用时被唤醒(称为 "虚假唤醒"),因此必须用带谓词的 wait,谓词用于验证条件是否真正成立,确保线程安全。


二、std::atomic:无锁原子操作

2.1 核心作用与原理

std::atomic 提供无锁的原子操作 ,底层依赖硬件的原子指令(如 x86 的 LOCK 前缀),比 std::mutex 更轻量,适合简单的共享数据(如计数器、标志位),避免了锁的开销和死锁风险。

2.2 基本用法

cpp 复制代码
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>

// 1. 原子计数器:无锁实现,多线程安全
std::atomic<int> atomic_count(0);
// 对比:非原子计数器(会有数据竞争)
int unsafe_count = 0;

void atomic_increment() {
    for (int i = 0; i < 10000; ++i) {
        atomic_count++; // 原子自增操作,底层是硬件原子指令
    }
}

void unsafe_increment() {
    for (int i = 0; i < 10000; ++i) {
        unsafe_count++; // 非原子操作,多线程下会出错
    }
}

int main() {
    // 测试原子计数器
    std::vector<std::thread> threads1;
    for (int i = 0; i < 10; ++i) {
        threads1.emplace_back(atomic_increment);
    }
    for (auto& t : threads1) t.join();
    std::cout << "[原子计数器] 最终结果: " << atomic_count << "(期望 100000,实际一致)\n";

    // 测试非原子计数器
    std::vector<std::thread> threads2;
    for (int i = 0; i < 10; ++i) {
        threads2.emplace_back(unsafe_increment);
    }
    for (auto& t : threads2) t.join();
    std::cout << "[非原子计数器] 最终结果: " << unsafe_count << "(期望 100000,实际可能更小)\n";

    // 2. 原子标志位:用于线程间的停止信号
    std::atomic<bool> stop_flag(false);
    std::thread worker([&stop_flag]() {
        while (!stop_flag) {
            std::cout << "[工作线程] 运行中...\n";
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
        }
        std::cout << "[工作线程] 收到停止信号,退出\n";
    });

    std::this_thread::sleep_for(std::chrono::seconds(2));
    stop_flag = true; // 原子设置标志位,通知工作线程停止
    worker.join();

    return 0;
}

2.3 内存序简介(进阶)

std::atomic 支持不同的内存序(Memory Order) ,用于控制多线程下的指令重排和可见性,默认是 std::memory_order_seq_cst(顺序一致性,最安全但性能稍低),其他内存序(如 memory_order_relaxedmemory_order_acquirememory_order_release)可用于优化性能,但需谨慎使用(容易出错)。


三、线程池:高效的线程管理

3.1 为什么需要线程池?

直接使用 std::thread 有两个核心问题:

  1. 频繁创建 / 销毁线程开销大:线程创建需要分配栈空间、内核态切换,销毁也需要回收资源,短任务场景下开销甚至超过任务本身。
  2. 线程数量不可控:无限制创建线程会导致系统资源耗尽、CPU 调度开销过大。

线程池 的核心思想是:预创建一组线程(固定数量或动态调整),将任务提交到任务队列,线程池中的线程循环从队列取任务执行,避免了频繁创建 / 销毁线程的开销,同时控制了线程数量。

3.2 线程池的核心组成

一个完整的线程池包含以下部分:

  1. 线程数组:预创建的工作线程集合。
  2. 任务队列:存储待执行任务的队列(需用 mutex 和条件变量保护)。
  3. 同步机制std::mutex 保护任务队列,std::condition_variable 通知工作线程有新任务。
  4. 任务提交接口 :支持提交任意可调用对象(函数、lambda、成员函数等),并返回 std::future 获取任务结果。

3.3 完整实现:一个可提交任务、获取结果的线程池

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <functional>
#include <future>
#include <memory>
#include <stdexcept>

class ThreadPool {
public:
    // 构造函数:创建指定数量的工作线程
    ThreadPool(size_t num_threads) : stop(false) {
        for (size_t i = 0; i < num_threads; ++i) {
            // 每个工作线程循环执行:从任务队列取任务并执行
            workers.emplace_back([this]() {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        // 阻塞直到:线程池停止 或 有新任务
                        this->condition.wait(lock, [this]() {
                            return this->stop || !this->tasks.empty();
                        });
                        // 线程池停止且任务队列为空,退出线程
                        if (this->stop && this->tasks.empty()) return;
                        // 从队列取任务
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }
                    // 执行任务
                    task();
                }
            });
        }
    }

    // 析构函数:停止所有线程
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true; // 设置停止标志
        }
        condition.notify_all(); // 唤醒所有工作线程
        for (std::thread& worker : workers) {
            worker.join(); // 等待所有线程退出
        }
    }

    // 禁止拷贝和移动
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;

    // 核心接口:提交任务,返回 std::future 获取结果
    template<class F, class... Args>
    auto submit(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {
        using return_type = typename std::result_of<F(Args...)>::type;

        // 将任务包装为 std::packaged_task,支持获取返回值
        auto task = std::make_shared<std::packaged_task<return_type()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );

        std::future<return_type> res = task->get_future();
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            // 线程池停止后禁止提交任务
            if (stop) throw std::runtime_error("submit on stopped ThreadPool");
            // 将任务放入队列(包装为 std::function<void()>)
            tasks.emplace([task]() { (*task)(); });
        }
        condition.notify_one(); // 通知一个工作线程有新任务
        return res;
    }

private:
    std::vector<std::thread> workers;        // 工作线程数组
    std::queue<std::function<void()>> tasks; // 任务队列
    std::mutex queue_mutex;                   // 保护任务队列的互斥锁
    std::condition_variable condition;        // 条件变量:通知工作线程
    bool stop;                                // 停止标志
};

// ------------------------------ 线程池使用示例 ------------------------------
// 示例1:简单的无返回值任务
void print_task(int id) {
    std::cout << "[线程池] 执行任务 " << id << ",线程ID: " << std::this_thread::get_id() << "\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

// 示例2:有返回值的任务
int add_task(int a, int b) {
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
    return a + b;
}

int main() {
    // 创建线程池:4个工作线程
    ThreadPool pool(4);
    std::cout << "[主线程] 线程池已启动,工作线程数量: 4\n";

    // 提交10个无返回值任务
    std::cout << "\n[主线程] 提交10个无返回值任务...\n";
    for (int i = 0; i < 10; ++i) {
        pool.submit(print_task, i + 1);
    }

    // 提交3个有返回值任务,用 std::future 获取结果
    std::cout << "\n[主线程] 提交3个有返回值任务...\n";
    std::vector<std::future<int>> futures;
    futures.push_back(pool.submit(add_task, 10, 20));
    futures.push_back(pool.submit(add_task, 30, 40));
    futures.push_back(pool.submit(add_task, 50, 60));

    // 获取并打印有返回值任务的结果
    std::cout << "\n[主线程] 等待有返回值任务完成...\n";
    for (size_t i = 0; i < futures.size(); ++i) {
        std::cout << "[主线程] 任务 " << i + 1 << " 结果: " << futures[i].get() << "\n";
    }

    // 主线程等待一段时间,让无返回值任务执行完毕
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "\n[主线程] 所有任务执行完毕,线程池将自动析构\n";

    return 0;
} // 线程池析构,自动停止所有工作线程

3.4 关键细节

任务包装 :用 std::packaged_taskstd::bind 将任意可调用对象包装为无参数的 std::function<void()>,同时支持通过 std::future 获取返回值。

线程安全 :任务队列的访问必须用 std::mutex 保护,条件变量用于通知工作线程有新任务或线程池停止。

析构安全 :析构函数中先设置 stop 标志,再唤醒所有线程,最后 join() 等待线程退出,确保线程池安全销毁。


四、总结与最佳实践

  1. std::condition_variable :用于线程间协作通知,必须与 std::unique_lock 配合,优先用带谓词的 wait() 处理虚假唤醒,典型场景是生产者 - 消费者模型。
  2. std::atomic :用于无锁原子操作,比 mutex 轻量,适合简单共享数据(计数器、标志位),默认内存序 memory_order_seq_cst 最安全,进阶可按需调整。
  3. 线程池:是多线程开发的核心工具,通过预创建线程避免频繁创建 / 销毁开销,核心是 "任务队列 + 工作线程",支持提交任意任务并获取结果,适合大量短任务场景。

掌握这三大知识点,能让你高效编写高性能、线程安全的 C++ 多线程程序。

相关推荐
lkforce2 小时前
MiniMind学习笔记(二)--model_minimind.py
笔记·python·学习·minimind·minimindconfig
瞎折腾啥啊2 小时前
CMake FetchContent与ExternalProject
c++·cmake·cmakelists
阿巴斯甜2 小时前
Predicate的使用:
java
阿巴斯甜2 小时前
Supplier的使用:
java
阿巴斯甜2 小时前
Function 用法:
java
三品吉他手会点灯3 小时前
STM32 VSCode 开发-C/C++的环境配置中,找不到C/C++: Edit Configurations选项
c语言·c++·vscode·stm32·单片机·嵌入式硬件·编辑器
来自远方的老作者3 小时前
第10章 面向对象-10.4 继承
开发语言·python·继承·单继承·多继承·super函数
做个文艺程序员3 小时前
流式输出(SSE)在 Spring Boot 中的实现【OpenClAW + Spring Boot 系列 第3篇】
java·spring boot·后端
逻辑驱动的ken3 小时前
Java高频面试考点场景题09
java·开发语言·数据库·算法·oracle·哈希算法·散列表