C++笔记---并发支持库(thread、mutex、condition_variable)
1. thread
std::thread 是 C++11 标准引入的跨平台线程库组件,定义在 <thread> 头文件中,用于封装操作系统底层线程,简化多线程编程(替代传统的 pthread(Linux)、CreateThread(Windows)等平台相关 API)。
其核心目标是让开发者以统一的接口创建、管理线程,无需关注底层系统差异。
实际上,thread类的接口与使用方法与Linux下的pthread库十分相似:Linux笔记---线程控制
1.1 创建线程对象(启动线程)
std::thread 的构造函数接收一个可调用对象(函数、lambda、函数对象、成员函数)作为线程入口,后续参数作为该函数的参数。
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
例如,创建两个线程分别执行 countDown函数:
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void countDown(int n, int fin)
{
for (int i = n; i >= fin; i--)
{
cout << this_thread::get_id() << ": " << i << endl;
this_thread::sleep_for(chrono::seconds(1));
}
}
int main()
{
thread t1(countDown, 10, 1);
thread t2(countDown, 20, 11);
cout << "t1: " << t1.get_id() << endl;
cout << "t2: " << t2.get_id() << endl;
t1.join();
t2.join();
return 0;
}
1.2 线程管理核心接口
std::thread 提供三个关键接口管理线程生命周期:join()、detach()、joinable()(可能是考虑安全原因,并没有提供曲线线程的 cancel() 接口)。
- join():阻塞等待子线程完成
- 调用后,当前线程(如主线程)会阻塞,直到子线程执行完毕。
- 子线程结束后,会回收其资源(避免 “僵尸线程”)。
- 只能调用一次,重复调用会导致未定义行为(程序崩溃)。
- detach():分离线程,后台运行
- 调用后,子线程与当前线程 “分离”,成为后台线程(守护线程)。
- 当前线程无需等待子线程,继续执行;子线程结束后,系统自动回收资源。
- 分离后,std::thread 对象不再关联任何线程(joinable() 变为 false),不可再调用 join()。
- 注意:若主线程先退出,分离的子线程可能被系统强制终止(取决于操作系统),需确保子线程不依赖主线程的局部变量。
- joinable():判断线程是否可管理
- 返回 true:线程处于可 join() 或 detach() 状态(未调用过 join()/detach(),且不是空线程)。
- 返回 false:空线程(默认构造的 std::thread)、已 join()/detach() 的线程。
- 用途:避免重复 join()/detach() 导致的崩溃,或在析构前判断是否需要处理线程。
1.3 按引用传参
std::thread 构造时,如果需要引用传参,必须用 std::ref() 或 std::cref() 包装参数:
#include <thread>
#include <iostream>
#include <functional> // std::ref
void modify_val(int& x) {
x = 999; // 修改主线程的变量
}
int main() {
int a = 10;
std::thread t(modify_val, std::ref(a));
t.join();
std::cout << a << std::endl; // 输出 999
return 0;
}
由于各个平台系统调用的规范的线程入口不同,std::thread 构造时,会先将用户传入的实参(Args&&...)通过 完美转发(std::forward<Args>)打包成 tuple,然后再通过特定的方式传递给该平台规范的线程入口。
这个tuple是在构造函数当中创建的临时对象,在向底层传递时,会使用移动语义来减少开销。
并且最终在对应平台的规范线程入口函数中,使用如下方式调用用户希望的线程入口函数:
// 简化的底层逻辑:子线程中展开 tuple 并调用目标函数
template <typename F, typename Tuple>
void thread_entry_point(F&& f, Tuple&& tup) {
// std::apply 展开 tuple,传递的是右值(std::move(tup))
std::apply(std::forward<F>(f), std::move(tup));
}
这就导致传入的参数本质上都是右值,若采用非const引用的方式接收参数会直接报错,而即便采用const引用的方式接收参数,也是引用到一个临时对象。
而std::ref会返回一个 std::reference_wrapper<T> 类型的引用包装器(本质是一个轻量级对象,内部存储指向原始对象的指针)。
当 std::thread 在子线程中调用目标函数时,std::reference_wrapper<T> 会被 隐式转换为 T&(通过其 operator T&() 重载),最终目标函数拿到的是对 原始对象的引用,而非副本。
1.4 std::this_thread 命名空间
std::this_thread 提供操作当前线程的工具函数,常用接口:
| 接口 | 功能描述 |
| get_id() | 获取当前线程的唯一 ID(std::thread::id 类型) |
| sleep_for(duration) | 让当前线程休眠指定时间(如 std::chrono::seconds(1)) |
| sleep_until(time_point) | 让当前线程休眠到指定时间点 |
| yield() | 让出 CPU 时间片,允许其他线程运行(不阻塞) |
注:
sleep_for() 和 sleep_until() 两个接口的参数涉及到C++11 引入的 标准时间库 std::chrono。
有兴趣的小伙伴可以自行了解,在这两个接口中,我们最常用的还是sleep_for()。
sleep_for() 的参数 std::chrono::duration 是模板类,用于表示「一段时间间隔」,定义如下:
template <typename Rep, typename Period = std::ratio<1>>
class duration;
- Rep:计数类型(如 int、long long、double),表示「Period 的倍数个数」。
- Period:周期(std::ratio 类型),表示每个计数单位对应的「秒数」,默认是 std::ratio<1>(1 秒),std::ratio<1, 10>表示1/10秒。
为了使用方便,标准库提供了常用时长的别名(直接用即可):
| 类型 | 含义 | Period |
| nanoseconds | 纳秒 | std::ratio<1, 1e9> |
| microseconds | 微秒 | std::ratio<1, 1e6> |
| milliseconds | 毫秒 | std::ratio<1, 1e3> |
| seconds | 秒 | std::ratio<1> |
| minutes | 分钟 | std::ratio<60> |
| hours | 小时 | std::ratio<3600> |
| days(C++20) | 天 | std::ratio<86400> |
| weeks(C++20) | 周 | std::ratio<604800> |
时长的常用操作:
- 创建时长:直接用预定义类型初始化,或用 std::chrono::duration_cast 转换。
- 算术运算:支持 +、-、*、/(如 1s + 500ms = 1500ms)。
- 获取计数:用 count() 方法获取时长的计数值(如 1s.count() = 1,500ms.count() = 500)。
#include <chrono>
#include <iostream>
using namespace std::chrono; // 简化命名空间
int main() {
// 1. 创建时长
seconds s(1); // 1 秒
milliseconds ms(500); // 500 毫秒
microseconds us = 1000us; // C++14 字面量(需启用 -std=c++14)
// 2. 算术运算
auto total = s + ms; // 1500 毫秒(类型为 duration<long long, ratio<1,1000>>)
std::cout << "total ms: " << total.count() << std::endl; // 输出 1500
// 3. 时长转换(不同周期需显式转换,用 duration_cast)
auto s2 = duration_cast<seconds>(total); // 1 秒(500ms 被截断)
std::cout << "total s: " << s2.count() << std::endl; // 输出 1
// 4. 浮点数时长(支持小数间隔)
duration<double, seconds::period> s3 = 1.5s; // 1.5 秒
std::cout << "1.5s in ms: " << duration_cast<milliseconds>(s3).count() << std::endl; // 1500
return 0;
}
2. mutex
std::mutex 是 C++11 标准库引入的基础互斥量(mutual exclusion,互斥),定义在 <mutex> 头文件中,核心作用是保护共享资源,防止多个线程同时访问导致的数据竞争(data race),确保同一时间只有一个线程能进入临界区(访问共享资源的代码段)。
同样地,接口上的设计与 pthread 库十分相似:Linux笔记---线程同步与互斥
2.1 创建mutex对象
mutex只有一个默认的构造函数,在使用时直接实例化即可:
在 pthread 库中,我们可以将锁设置为递归锁或超时锁,mutex可不可以呢?
答案是不可以,mutex就是一个最基础的互斥量。在C++11中,专门提供了这些特殊锁类型:
| 互斥量类型 | 特点 |
| std::mutex | 非递归、无超时、基础功能 |
| std::recursive_mutex | 可递归(同一线程多次加锁) |
| std::timed_mutex | 非递归、支持超时加锁(try_lock_for) |
| std::recursive_timed_mutex | 可递归、支持超时加锁 |
2.2 常用接口
| 接口 | 功能描述 |
| lock() | 阻塞式加锁:若锁未被持有,当前线程获取锁;若已被持有,当前线程阻塞至锁释放。 |
| try_lock() | 非阻塞式加锁:尝试获取锁,成功返回 true,失败返回 false(不阻塞)。 |
| unlock() | 释放锁:必须由持有锁的线程调用,否则行为未定义(UB)。 |
| native_handle() | 返回原生互斥量句柄(如 POSIX 下的 pthread_mutex_t),用于平台相关操作(慎用)。 |
除了上面的接口,含 timed 前缀的锁类型还支持以下两个接口:
| 接口 | 功能描述 |
| try_lock_for(duration) | 至多阻塞等待锁duration时间,若在该时间段内未获取到锁,则放弃。 |
| try_lock_until(time_point) | 至多阻塞等待锁到time_point时间点,若在到达该时间点前未获取到锁,则放弃。 |
2.3. RAII 包装器
和内存的申请释放同理,锁的申请释放也推荐使用RAII的思想来管理。
C++ 提供了 std::lock_guard、std::unique_lock 等 RAII 工具类(定义于 <mutex> 头文件),自动管理加锁 / 解锁(构造时加锁,析构时解锁),避免手动操作的遗漏,是实际开发的首选。
它们的核心差异在于 灵活性和开销:std::lock_guard 是 “轻量简单款”,std::unique_lock 是 “灵活全能款”。
2.3.1 lock_guard
std::lock_guard 的构造函数仅支持 “直接加锁” 和 “接管已加锁的互斥量”,参数需传递互斥量的引用(不可传值,否则会拷贝互斥量,而 std::mutex 不可拷贝)。
| 构造函数原型 | 功能描述 | 注意事项 |
| explicit lock_guard(mutex_type& m) | 默认构造:传入互斥量 m,构造时自动调用 m.lock() 加锁。 | 最常用形式,互斥量需是外部定义的(如全局、类成员、智能指针管理的)。 |
| lock_guard(mutex_type& m, std::adopt_lock_t tag) | 接管已加锁的互斥量:假设调用者已手动调用 m.lock(),构造时不重复加锁,仅接管解锁权。 | 必须确保 m 已被当前线程加锁,否则析构时调用 unlock() 会导致 UB(未定义行为)。 |
std::mutex mtx;
void func1() {
std::lock_guard<std::mutex> lock(mtx);
shared_val *= 2; // 临界区
}
void func2() {
mtx.lock();
// 接管已加锁的mtx,构造时不重复加锁,析构时自动解锁
std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
shared_val *= 2; // 临界区
}
2.3.2 unique_lock
std::unique_lock 是功能最丰富的 RAII 锁包装器,支持手动控制锁状态、延迟加锁、超时加锁、所有权转移等高级特性,代价是比 std::lock_guard 多一点内存开销和执行开销。
它的核心设计是 “独占所有权”(同一时间仅一个 unique_lock 对象管理某个互斥量),但支持移动语义(可将锁的所有权转移给其他 unique_lock 对象)。
| 构造函数原型(简化) | 锁策略 | 功能描述 |
| explicit unique_lock(mutex_type& m) | 默认加锁 | 构造时调用 m.lock(),阻塞直到获取锁。 |
| unique_lock(mutex_type& m, std::defer_lock_t) | 延迟加锁 | 构造时不加锁,仅关联互斥量,后续手动调用 lock()。 |
| unique_lock(mutex_type& m, std::adopt_lock_t) | 接管已加锁 | 构造时不加锁,接管当前线程已持有的锁,析构时解锁。 |
| unique_lock(mutex_type& m, std::try_to_lock_t) | 尝试加锁 | 构造时调用 m.try_lock(),成功返回持有锁,失败返回不持有(不阻塞)。 |
| unique_lock(mutex_type& m, const chrono::duration& d) | 超时加锁 | 构造时调用 m.try_lock_for(d),超时未获取则不持有锁(需互斥量支持超时)。 |
相比于 std::lock_guard,std::unique_lock 为了进行更加灵活的控制,提供了丰富的成员函数。
其中包括对底层锁主要接口的封装,可直接使用相同的接口对底层的锁进行控制:
除此之外,还提供了如下的常用接口:
| 成员函数 | 功能描述 |
| owns_lock() | 返回 bool:当前对象是否持有锁(避免重复加锁 / 解锁)。 |
| mutex() | 返回指向底层互斥量的引用(慎用,避免手动操作互斥量)。 |
| release() | 释放对互斥量的管理:返回底层互斥量指针,后续 unique_lock 析构时不解锁。 |
| operator bool() | 同owns_lock(),可以直接将unique_lock对象作为条件表达式。 |
2.4 lock和try_lock
为什么要有“接管已加锁的互斥量”和“延迟加锁”这样的设计呢?这样设计主要是为了和std::lock以及std::try_lock配合,以避免死锁:

这两个函数的作用是同时对参数列表中的所有锁变量进行加锁操作。
std::lock内部通过特定算法(如 “按互斥量地址排序后加锁”),确保多个线程以相同顺序获取锁,从根源避免死锁。
std::try_lock尝试获取所有传入的互斥量,不会阻塞—— 若所有互斥量都可获取,则成功,返回 -1;若任意一个不可获取,则立即放弃,释放已获取的锁,返回第一个获取失败的互斥量的索引(从 0 开始)。
例如cplusplus.com中给出的这个示例:
// unique_lock constructor example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock, std::unique_lock
// std::adopt_lock, std::defer_lock
std::mutex foo,bar;
void task_a () {
std::lock (foo,bar); // simultaneous lock (prevents deadlock)
std::unique_lock<std::mutex> lck1 (foo,std::adopt_lock);
std::unique_lock<std::mutex> lck2 (bar,std::adopt_lock);
std::cout << "task a\n";
// (unlocked automatically on destruction of lck1 and lck2)
}
void task_b () {
// foo.lock(); bar.lock(); // replaced by:
std::unique_lock<std::mutex> lck1, lck2;
lck1 = std::unique_lock<std::mutex>(bar,std::defer_lock);
lck2 = std::unique_lock<std::mutex>(foo,std::defer_lock);
std::lock (lck1,lck2); // simultaneous lock (prevents deadlock)
std::cout << "task b\n";
// (unlocked automatically on destruction of lck1 and lck2)
}
int main ()
{
std::thread th1 (task_a);
std::thread th2 (task_b);
th1.join();
th2.join();
return 0;
}
3. condition_variable
std::condition_variable 是 C++11 引入的同步原语(条件变量),定义在 <condition_variable> 头文件中,用于线程间的条件等待:让一个 / 多个线程等待某个条件满足,直到其他线程通知其条件成立。
与pthread库中的条件变量有所不同的是,condition_variable的成员函数并不直接以锁为参数,而是接收一个std::unique_lock对象。
除此之外,用法几乎与pthread库中的条件变量相同,下面是常用的成员函数:
| 接口 | 说明 |
| wait(unique_lock&) | 解锁互斥锁,阻塞等待;被唤醒后重新加锁,需循环检查条件。 |
| wait(lock, pred) | 带谓词的等待(推荐),等价于 while(!pred()) { wait(lock); }。 |
| wait_for(lock, dur, pred) | 等待指定时长,超时或条件满足时返回,返回值为 bool(是否满足条件)。 |
| wait_until(lock, time, pred) | 等待到指定时间点,逻辑同 wait_for。 |
| notify_one() | 唤醒一个等待的线程(若有)。 |
| notify_all() | 唤醒所有等待的线程(若有)。 |
示例代码(两个线程合作按序打印1~100,一个线程打印奇数,一个线程打印偶数):
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
int main()
{
// true打印偶数,false打印奇数
bool flag = true;
mutex mtx;
condition_variable cond;
// 线程1打印偶数
thread t1([&]() {
int i = 0;
while (i <= 100)
{
unique_lock<mutex> lck(mtx);
while (!flag)
cond.wait(lck);
cout << this_thread::get_id() << ": " << i << endl;
i += 2;
flag = false;
cond.notify_one();
}
});
// 线程2打印奇数
thread t2([&]() {
int i = 1;
while (i <= 100)
{
unique_lock<mutex> lck(mtx);
while (flag)
cond.wait(lck);
cout << this_thread::get_id() << ": " << i << endl;
i += 2;
flag = true;
cond.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
除了condition_variable,标准库中还提供了condition_variable_any。相比于前者,后者的成员函数可以接收任意类型的锁对象(只要提供了lock和unlock成员函数)。
4. call_once
std::call_once 是 C++11 标准库提供的 线程安全的一次性初始化工具(定义于 <mutex> 头文件),核心作用是保证某个函数在多线程环境下仅执行一次,常用于全局资源初始化、单例模式实现等场景。
它与 std::once_flag 配合使用,底层通过互斥量和原子操作实现,比手动用 std::mutex + bool 标记 更简洁、安全(避免内存可见性问题和竞态条件)。
// call_once example
#include <iostream> // std::cout
#include <thread> // std::thread, std::this_thread::sleep_for
#include <chrono> // std::chrono::milliseconds
#include <mutex> // std::call_once, std::once_flag
int winner;
void set_winner (int x) { winner = x; }
std::once_flag winner_flag;
void wait_1000ms (int id) {
// count to 1000, waiting 1ms between increments:
for (int i=0; i<1000; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
// claim to be the winner (only the first such call is executed):
std::call_once (winner_flag,set_winner,id);
}
int main ()
{
std::thread threads[10];
// spawn 10 threads:
for (int i=0; i<10; ++i)
threads[i] = std::thread(wait_1000ms,i+1);
std::cout << "waiting for the first among 10 threads to count 1000 ms...\n";
for (auto& th : threads) th.join();
std::cout << "winner thread: " << winner << '\n';
return 0;
}
更多推荐


所有评论(0)