C++ 学习 多线程 2025年6月17日18:41:30

多线程(标准线程库 <thread>)

创建线程

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

void hello() {
    std::cout << "Hello from thread!\n";
}

int main() {
     // 创建线程并执行 hello() 
    std::thread t(hello); //线程对象,传入可调用对象(函数、Lambda、函数对象)
    t.join();              // 等待线程结束 阻塞主线程,直到子线程完成。
//t.detach():分离线程(线程独立运行,主线程不等待)。
// 仅在明确不需要管理线程生命周期时使用 detach(),并确保资源安全。
    return 0;
}
传递参数(和平常函数调用不同 注意看):
cpp 复制代码
void print_sum(int a, int b) {
    std::cout << a + b << "\n";
}

int main() {
    std::thread t(print_sum, 10, 20);  // 传递参数
    t.join();
}
Lambda 表达式 线程:
cpp 复制代码
std::thread t([] {
    std::cout << "Lambda thread\n";
});
t.join();

线程同步:

互斥锁(Mutex):防止多个线程访问 共享数据:
cpp 复制代码
#include <mutex>

std::mutex mtx;
int shared_data = 0;

//原始手动加锁 
void increment() {
    mtx.lock();          // 加锁:如果其他线程已锁,这里会阻塞等待
    shared_data++;       // 临界区:唯一线程能执行的代码
    mtx.unlock();        // 解锁:允许其他线程进入
}
//风险:如果 shared_data++ 抛出异常,unlock() 可能不被执行,导致死锁。



void safe_increment() {
    std::lock_guard<std::mutex> lock(mtx);  // 构造时自动加锁
    shared_data++;                          // 临界区
} // 析构时自动解锁(即使发生异常)
//RAII(资源获取即初始化):利用对象生命周期自动管理锁,避免忘记解锁。

加锁后的正确流程
线程A加锁 → shared_data++(变为1)→ 解锁

线程B加锁 → shared_data++(变为2)→ 解锁
结果:shared_data = 2。
高阶用法:
cpp 复制代码
void flexible_increment() {
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁
    // ...其他非临界区代码...
    lock.lock();       // 手动加锁
    shared_data++;
    lock.unlock();     // 可手动提前解锁
}

=================================================

//多个锁时,按固定顺序获取:
std::mutex mtx1, mtx2;

void safe_operation() {
    std::lock(mtx1, mtx2); // 同时加锁(避免死锁)
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    // 操作多个共享资源
}

=============错误示范===============
int* get_data() {
    std::lock_guard<std::mutex> lock(mtx);
    return &shared_data; // ❌ 危险!锁失效后仍可访问
}

注意事项
锁粒度:锁的范围应尽量小(减少阻塞时间)。

避免嵌套锁:容易导致死锁。

不要返回锁保护的指针/引用:会破坏封装性。
线程之间通知机制 (条件变量)
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;  // 条件变量依赖的共享状态

// 消费者线程(等待数据)
void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    std::cout << "消费者: 等待数据...\n";
    cv.wait(lock, [] { return data_ready; });  // 等待条件成立
    std::cout << "消费者: 收到数据,开始处理!\n";
}

// 生产者线程(准备数据)
void producer() {
    std::this_thread::sleep_for(std::chrono::seconds(1));  // 模拟耗时操作
    {
        std::lock_guard<std::mutex> lock(mtx);
        data_ready = true;  // 修改共享状态
        std::cout << "生产者: 数据已准备!\n";
    }
    cv.notify_one();  // 通知等待的消费者线程
}

int main() {
    std::thread t1(consumer);  // 消费者线程(等待)
    std::thread t2(producer);  // 生产者线程(通知)

    t1.join();
    t2.join();
    return 0;
}

//输出效果 
消费者: 等待数据...
生产者: 数据已准备!
消费者: 收到数据,开始处理!




关键点解析
条件变量 (std::condition_variable)

用于线程间的条件同步,允许线程阻塞直到某个条件成立。

必须与 std::mutex 和 一个共享状态变量(如 bool data_ready)配合使用。
-------------------------------------------------------------------------
cv.wait(lock, predicate) 的工作原理

步骤1:线程获取锁后检查条件(predicate)。

步骤2:若条件为 false,线程释放锁并进入阻塞状态,等待通知。

步骤3:当其他线程调用 notify_one() 时,线程被唤醒,重新获取锁并再次检查条件。

步骤4:若条件为 true,线程继续执行;否则继续等待。

为什么需要 data_ready 变量?

避免虚假唤醒:操作系统可能意外唤醒线程,因此需要显式检查条件。

状态同步:明确线程间的通信意图(如"数据已准备好")。

锁的作用域

生产者:修改 data_ready 时必须加锁(lock_guard)。

消费者:wait() 会自动释放锁,唤醒后重新获取锁。

异步任务(得重点掌握):

std::async

异步执行函数,返回 std::future

cpp 复制代码
#include <future>

int compute() { return 42; }

int main() {
    std::future<int> result = std::async(compute);
    std::cout << "Result: " << result.get() << "\n";  // 阻塞获取结果
}

std::launch::async:立即异步执行。

std::launch::deferred:延迟到 get() 时执行。

=====================================================================

std::packaged_task

将函数包装为可异步调用的任务:

cpp 复制代码
std::packaged_task<int()> task([] { return 7 * 6; }); //异步包装
std::future<int> result = task.get_future(); //future 用于稍后获取异步结果。
std::thread t(std::move(task));  // 在线程中执行
t.join();
std::cout << "Result: " << result.get() << "\n";

==================================================
通过 std::move:

将 task 的所有权转移给线程 t,避免拷贝。

转移后,原 task 对象变为 空状态(不能再调用)

线程管理

获取硬件线程数:

std::thread::hardware_concurrency()

返回的是 当前计算机硬件支持的线程并发数(通常等于逻辑CPU核心数)

线程不超过线程数时 效果最佳

cpp 复制代码
unsigned cores = std::thread::hardware_concurrency();
std::cout << "CPU cores: " << cores << "\n";
//std::thread::hardware_concurrency() 返回的是 
//当前计算机硬件支持的线程并发数(通常等于逻辑CPU核心数)

4 核 4 线程 CPU → 输出 4

4 核 8 线程 CPU → 输出 8

苹果 M1 Max (10 核) → 输出 10
线程局部储存(每个线程独享的变量TLS):
cpp 复制代码
thread_local int counter = 0;  // 每个线程有独立副本

原子操作(无须锁的安全操作)

为什么不需要锁?

1.硬件支持

  • CPU 原子指令 :现代 CPU 提供专门的指令(如 x86 的 LOCK XADD、ARM 的 LDREX/STREX)确保单条指令完成"读取-修改-写入"操作,不会被线程切换打断。

  • 缓存一致性协议:通过 MESI 等协议保证多核间对原子变量的可见性。


2. 编译器与语言标准保障

  • 编译器屏障std::atomic 操作会阻止编译器重排序相关指令。

  • 内存顺序控制 :支持灵活的内存序(如 memory_order_relaxedmemory_order_seq_cst),平衡性能与一致性需求。

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

std::atomic<int> counter(0);

void increment(int n) {
    for (int i = 0; i < n; ++i) {
        counter++;  // 原子自增
    }
}

int main() {
    std::thread t1(increment, 100000);
    std::thread t2(increment, 100000);
    t1.join(); t2.join();
    std::cout << "Counter: " << counter << "\n";  // 保证输出 200000
    return 0;
}
特性 std::atomic std::mutex
实现层级 硬件指令 + 编译器优化 操作系统级锁(可能涉及系统调用)
粒度 单个变量操作 保护任意代码块
性能 极高(无锁设计) 较高(存在锁争用开销)
适用场景 简单变量(int、bool、指针等) 复杂逻辑或跨多个变量的操作
std::atomic 的局限性
  • 仅适用于标量类型:对结构体等复杂类型需自定义或使用锁。

  • 内存序选择 :错误的内存序可能导致意外行为(如 memory_order_relaxed 不保证顺序)。

死锁预防:

避免嵌套锁:按固定顺序加锁。

使用 std::lock 同时锁多个互斥量(前面互斥锁有拓展)

cpp 复制代码
std::mutex mtx1, mtx2;
std::lock(mtx1, mtx2);  // 同时加锁(避免死锁)
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);

线程池(运用第三方库实现)广泛应用于需要高并发处理短任务的场景(如HTTP服务器、并行计算等)

第三方库(如 BS::thread_pool):
cpp 复制代码
#include "thread_pool.hpp"          // 引入线程池库头文件
BS::thread_pool pool;              // 创建默认线程池(线程数=硬件并发数)
auto task = pool.submit([] { return 42; });  // 提交Lambda任务
std::cout << task.get() << "\n";   // 阻塞等待并获取结果

|-----------------------|--------------------------------------------|
| BS::thread_pool | 线程池类,管理一组工作线程(通常数量=CPU核心数)。 |
| pool.submit() | 提交任务(函数/Lambda)到线程池,返回 std::future 对象。 |
| task.get() | 阻塞调用线程,直到任务完成并返回结果(类似 std::future::get)。 |

工作流程
  1. 线程池初始化

    • 创建时默认启动 std::thread::hardware_concurrency() 个工作线程。

    • 线程空闲时会自动从任务队列中取任务执行。

  2. 任务提交

    • submit 将 Lambda [] { return 42; } 封装为任务,放入队列。

    • 返回的 task 是一个 std::future<int>,用于后续获取结果。

  3. 结果获取

    • task.get() 会阻塞主线程,直到某个工作线程完成该任务。

    • 最终输出 42

对比原生 std::thread
特性 BS::thread_pool std::thread
线程管理 自动复用线程(避免频繁创建/销毁) 需手动管理线程生命周期
任务队列 支持批量提交任务 需自行实现任务队列
开销 低(线程复用) 高(每次任务新建线程)
适用场景 大量短任务 少量长任务
拓展用法示例:
cpp 复制代码
//批量提交任务
std::vector<std::future<int>> results;
for (int i = 0; i < 10; ++i) {
    results.push_back(pool.submit([i] { return i * i; }));
}
for (auto& r : results) {
    std::cout << r.get() << " ";  // 输出 0 1 4 9 16 25 36 49 64 81
}

//获取线程池信息
std::cout << "线程数: " << pool.get_thread_count() << "\n";

//等待所有任务完成
pool.wait();  // 阻塞直到所有任务完成(不销毁线程)

总结

功能 工具 头文件
线程创建 std::thread <thread>
互斥锁 std::mutex, std::lock_guard <mutex>
条件变量 std::condition_variable <condition_variable>
异步任务 std::async, std::future <future>
原子操作 std::atomic <atomic>
线程局部存储 thread_local 语言内置
相关推荐
恒者走天下18 分钟前
秋招是开发算法一起准备,还是只准备一个
c++
charlie11451419126 分钟前
从C++编程入手设计模式——外观模式
c++·设计模式·外观模式
虾球xz38 分钟前
CppCon 2016 学习:The Exception Situation
开发语言·c++·学习
老土豆FUSK40 分钟前
C++ 封装特性
开发语言·c++
蒙奇D索大42 分钟前
【数据结构】图论实战:DAG空间压缩术——42%存储优化实战解析
数据结构·笔记·学习·考研·图论·改行学it
Cyrus_柯1 小时前
C++(面向对象编程)
开发语言·c++·算法·面向对象
ᥬ 小月亮1 小时前
Mongodb下载安装与使用(Windows版本)
windows·学习·mongodb
今天我要乾重生1 小时前
java基础学习(三十)
java·开发语言·学习
西岭千秋雪_2 小时前
计算机网络学习笔记:TCP流控、拥塞控制
网络·笔记·学习·tcp/ip·计算机网络