C++ 语言层面上的多线程

C++ 语言层面上的多线程
小米里的大麦C++ 语言层面上的多线程
1. C++11 的线程模型 vs pthread 的对应关系
| pthread API | C++11 对应写法 | 说明 |
|---|---|---|
pthread_create() | std::thread(func, args...) | 创建并启动线程 |
pthread_self() | std::this_thread::get_id() | 获取当前线程 ID |
pthread_join() | t.join() | 等待线程结束 |
pthread_detach() | t.detach() | 分离线程 |
pthread_exit() | return 即可 | 函数返回自动结束线程 |
pthread_cancel() | 无直接等价(需自定义退出标志) | C++ 不建议 强制终止线程 |
pthread_mutex_* | std::mutex / std::lock_guard | 自动 RAII 锁管理 |
pthread_cond_* | std::condition_variable | 条件变量 |
2. 线程的使用
1. 语法
1 | std::thread t(func, arg1, arg2, ...); |
可以传 任意个参数(0 个也行),只要函数能接收即可,默认是 值传递(参数 自动复制一份 传入线程中),如果要让线程内修改外部变量,使用 std::ref(变量)(头文件 <functional>) 包一层表示传引用,比如:std::ref(var),线程启动后立即执行传入函数。如果要创建一个 无参的线程对象(不执行任何函数,这种情况很少见),有两种正确写法:
- 省略括号:
std::thread t1;,此时会默认构造一个std::thread对象(不关联任何线程,joinable()返回false)。 - 使用统一初始化语法(C++11 起):
std::thread t1{};,显式调用默认构造函数,效果与std::thread t1;相同。
2. 代码示例
1 | void func(int& x) { x = 100; } |
1. 传普通函数
1 |
|
C++ 标准库要求:线程对象销毁前,必须显式处理线程的执行状态(要么
join等待其完成,要么detach让其后台运行)。如果不处理,线程对象销毁时会触发异常,导致程序abort(“Debug Error! abort () has been called”)。这是 C++ 标准委员会为了 防止资源泄漏和未定义行为 而做的安全设计:
std::thread是 RAII 对象,它的析构应该清理资源。如果不强制join或detach,程序员很容易忘记,导致线程悬空、资源泄露、甚至访问非法内存。相比于pthread的“自由放任”,std::thread更偏向“安全第一”。
2. 传引用参数(想修改主线程变量时)
1 |
|
不加 ref(),参数会被复制(传值),加 ref(),可修改主线程变量(传引用)。
3. 传 lambda(常用)
1 |
|
4. 传成员函数
1 |
|
3. join 和 detach
join():主线程等待子线程执行完毕,再继续执行。适用于需要同步的场景。detach():子线程与主线程 “分离”,在后台独立运行(主线程退出后,子线程可能继续执行,由系统回收资源)。适用于后台任务,不关心何时结束。
join 其实非常好理解和上手使用,但是 detach 有一个注意事项:detach() 使线程后台运行,不再与主线程同步,若主线程提前退出,资源可能已被销毁,线程访问将非法。所以:
join的使用场景: 需要等待线程执行完成的情况,需要获取线程执行结果的情况,主线程退出前必须确保子线程完成的情况。detach的使用场景: 线程需要长时间运行(如后台服务),主线程不需要等待线程完成,线程不需要返回结果给主线程。注意事项:detach 后无法再对线程进行 join,要确保 detach 的线程不会访问已经被释放的资源,一旦主线程从main()返回或程序调用exit(),整个进程将终止,此时所有后台线程(包括已 detach 的)都会被操作系统强制销毁,无论其是否完成。
4. 线程结束与终止机制
C++ 中线程终止的推荐方式与 Linux 的 pthread 有所不同:C++ 标准没有提供 pthread_cancel 那样的 外部强制终止线程,因为这种行为可能在 C++ 对象析构时造成资源泄露(破坏 RAII)。
| 方式 | 说明 | 推荐程度 |
|---|---|---|
return / 函数执行完 | 正常退出(推荐) | ⭐⭐⭐⭐⭐ |
| 标志位控制 | 用于特殊场景的控制 | ⭐⭐⭐ |
| 抛出异常(未捕获则线程终止) | 用于错误情况 | ⭐⭐⭐ |
调用 std::terminate() / exit() / _exit() | 会终止整个进程,而非仅线程 | ❌ 十分不推荐 |
代码示例:
1 | void thread_function() // 执行任务 |
正常退出的优点:自动调用线程函数中所有对象的析构函数、正确释放线程堆栈内存、设置正确的退出代码、递减线程内核对象使用计数。
1 | // 标志位控制线程退出: |
使用 原子标志位 控制线程逻辑:不通过强制“杀死线程”,而是让线程 自己检测退出信号。 这种方式资源安全、可控,是现代 C++ 推荐的线程终止模式。
1 |
|
5. 线程休眠
在 C++ 层面,不再使用 sleep() 或 usleep(),推荐下面 2 种,用于替代 Windows 的 Sleep() 和 Linux 的 usleep(),可跨平台。
| 函数 | 头文件 | 作用 |
|---|---|---|
std::this_thread::sleep_for(duration) | <thread> + <chrono> | 休眠指定时间 |
std::this_thread::sleep_until(time_point) | <thread> + <chrono> | 休眠到指定时间 |
using namespace std::chrono;:作用是引入std::chrono命名空间下的 时间类型和时钟类型,例如:- 时长类型:
std::chrono::seconds、std::chrono::milliseconds等。- 时钟类型:
std::chrono::system_clock、std::chrono::steady_clock等。- 时间点类型:
std::chrono::time_point等。using namespace std::chrono_literals;:作用是引入 C++14 新增的时间单位字面量(如s、ms、us、ns、min、h),可以用更简洁的方式表示时长,例如:1s等价于std::chrono::seconds(1)。500ms等价于std::chrono::milliseconds(500)。2min等价于std::chrono::minutes(2)。1h等价于std::chrono::hours(1)。
示例:
1 |
|
理解方式:std::this_thread::sleep_for(duration) 让当前线程休眠一段时间(“睡多久”),即延迟执行后面的代码。“sleep for X” = 从现在起暂停 X 时间再继续执行,假如当前时间是 10:00:00,sleep_for(3s) → 线程在 10:00:03 再醒。
1 |
|
这里
now是system_clock::time_point类型。
理解方式:std::this_thread::sleep_until(time_point) 让当前线程休眠到指定的时刻,即“睡到什么时候”。“sleep until T” = 睡到某个时间点 T,假设现在是 10:00:00,指定 10:00:03 → 线程睡到那时醒,若调用时系统时间已超过目标时间,函数立即返回。
1 |
|
6. 锁:std::mutex
C++ 互斥锁的三种常见用法
C++语言层面提供了三种常用锁,都在头文件 <mutex> 中:
| 锁类型 | 是否自动解锁 | 功能特点 | 推荐使用场景 |
|---|---|---|---|
std::mutex | ❌ 否 | 最基础的互斥锁,需手动 lock/unlock | 灵活控制锁时机 |
std::lock_guard<std::mutex> | ✅ 是 | 自动加锁、自动释放,作用域结束自动解锁 | 简单作用域加锁 |
std::unique_lock<std::mutex> | ✅ 是 | 功能更强(可延迟加锁、可解锁再加锁) | 条件变量、复杂锁逻辑 |
1. 手动加解锁(像 pthread)
1 | mutex mtx; |
注意:不要忘记 unlock(),异常时无法释放锁!若函数中有异常或提前 return,可能导致未解锁,不推荐在复杂逻辑中使用。如果临界区代码抛出异常,锁将无法释放,可能会导致死锁!
1 |
|
2. RAII 自动锁:std::lock_guard
std::lock_guard 是 最简单、最轻量、不能折腾的锁。
只能:
- 构造 → 自动
lock()。 - 析构 → 自动
unlock()。
不能:
- 中途 unlock
- 不能 lock
- 不能移动
- 不能延迟加锁
1 | mutex mtx; |
比较常用、安全的写法。lock_guard<mutex> 是一个模板类,<mutex> 指模板参数的类型,即“锁的类型”,必须是 mutex,不能写成 int 或 string,(mtx) 是构造参数,表示传入要保护的那把锁,离开作用域(函数返回、异常抛出等)时会 自动调用 unlock(),非常安全。
1 |
|
3. 可灵活控制的锁:std::unique_lock
std::unique_lock 是 功能全面版的锁,为了处理复杂情况设计的,它允许:
- 中途 unlock()
- 中途 lock()
- 可以使用 try_lock
- 可以延迟加锁(defer_lock)
- 可以转移所有权(移动语义)
- 可以和 condition_variable 协作(这是重点)
1 | mutex mtx; |
unique_lock<mutex> 也接受 mutex 类型作为模板参数,可以选择何时加锁、解锁,更灵活,常用于 condition_variable(条件变量)场景。
1 |
|
平时开发中,常用的差不多就这些,更多高深的较少用到,需要时简单查阅一下就行,扩展:了解一下递归锁,大概就是在递归中的用的锁,但是实际开发中递归也比较少用,所以这里不作为重点。
7. 原子操作:std::atomic
1. std::mutex vs std::atomic:两种完全不同的思路
| 对比点 | std::mutex(互斥锁) | std::atomic(原子变量) |
|---|---|---|
| 保护范围 | 一整段代码(临界区) | 一个变量本身 |
| 操作粒度 | 粗(可包住多行逻辑) | 细(只保护单个读写) |
| 原理 | 多线程竞争锁 → 内核或自旋控制 | CPU 提供原子指令实现 |
| 阻塞 | 会阻塞(等待锁) | 不阻塞或轻量阻塞(硬件原子指令) |
| 使用场景 | 复杂临界区,修改多个共享变量或复杂逻辑(多步骤操作、多变量共享) | 简单操作:计数、状态标志、单变量累加等 |
| 开销 | 较大(系统级锁,涉及内核态切换) | 极小(硬件原语,用户态操作,接近普通变量) |
| 可替代关系 | 原子操作不能代替锁保护复杂逻辑 | 锁可以保护一切,但性能低 |
mutex:锁的是“一段代码”(临界区)。atomic:锁的是“一个变量的单个读写”。
2. 为什么需要 atomic?
atomic的自增操作底层是硬件级原子指令(如LOCK XADD),不会被中断打断,也不会出现竞争。
因为有时候我们只是做一个 简单的共享数值操作(例如计数器),用互斥锁反而太重,std::atomic 直接让这些操作变为 无锁且线程安全。
mutex 版:
1 |
|
atomic 版:
1 |
|
3. atomic 的使用
1. 定义
头文件:
1 |
|
定义方式:
1 | atomic<int> cnt(0); // 定义原子整型变量并初始化成 0 |
2. 常用操作
| 操作 | 含义 | 示例 |
|---|---|---|
load() | 读取当前值 | int x = cnt.load(); |
store(val) | 写入值 | cnt.store(10); |
fetch_add(n) | 加法并返回旧值 | cnt.fetch_add(1); |
fetch_sub(n) | 减法并返回旧值 | cnt.fetch_sub(1); |
++、-- | 原子自增、自减 | ++cnt;、--cnt; |
exchange(val) | 原子地替换值 | cnt.exchange(100); |
compare_exchange_strong(expected, desired) | CAS 操作 | 如果当前值 == expected,则更新为 desired |
3. 代码示例
示例一:多线程计数器(替代 mutex)
1 |
|
示例二:线程通信(退出标志)
1 |
|
示例三:compare_exchange_strong(CAS),CAS(Compare And Swap)是底层原子操作的基础。
1 | std::atomic<int> val(10); |
1 |
|
4. 注意事项
- 不支持复杂操作: 原子类型仅保证 单个操作 的原子性,若要执行多步操作(如
a += b且b也是共享变量),仍需用mutex保护。 - 避免过度使用: 原子类型的性能虽高,但比普通变量仍有开销。若变量无需多线程共享,用普通类型即可。
- 类型限制: 并非所有类型都能作为
std::atomic的模板参数,仅支持 “可平凡复制” 的类型(如基本类型、指针、简单结构体)。复杂类型需用mutex保护。 - 内存序(Memory Order): 上述示例省略了内存序参数(默认
std::memory_order_seq_cst,最严格),复杂场景可通过指定内存序(如std::memory_order_relaxed)优化性能,目前用不到,需要时查一下即可。扩展:实际开发中,理解内存序对于编写正确的并发代码比较有帮助。














