c++11 的多线程
多线程编程
多线程编程,并发编程的一种,即在同一个进程中执行多个线程,每个线程都有独立的栈、指令计数器,但共享同一个内存空间(堆、全局变量等),可以让程序在多核 CPU 上并行执行,从而更快更高效喵!。
但是由于缺少系统的保护机制,多线程编程容易出现数据竞争和死锁等问题。
C++11 中的多线程
编译器版本:Clang 21.1.1
C++ 标准:C++11
多线程在 C++11 被引入,其工具集在 C++11 已经比较完善,主要分为五个板块:
- 线程管理:
thread - 互斥锁:
mutex - 线程同步:
condition_variable - 原子操作:
atomic - 异步操作:
future和async
thread 线程类
std::thread 单个执行线程的类,用于创建和管理线程。
构造函数
1 | std::thread t1; // 默认构造函数,默认不执行 |
成员函数
join():等待线程执行结束,阻塞当前线程,直到线程执行结束。detach():将线程与thread对象分离,允许线程独立执行(守护线程)。joinable:检查线程是否可被join(),即在运行且未被分离。get_id():获取线程的唯一标识符。hardware_concurrency():静态函数,返回系统硬件支持的并发线程数。
mutex 互斥锁类
std::mutex 保护共享数据,防止多个线程同时访问导致数据竞争。
std::mutex
最基本的互斥锁,不可递归锁定。
成员函数
lock():获取互斥锁,如果互斥锁已经被其他线程锁定,则阻塞当前线程,直到互斥锁被释放。try_lock():尝试获取互斥锁,如果互斥锁已经被其他线程锁定,则立即返回false,否则获取锁并返回true。unlock():释放互斥锁。
std::recursive_mutex
允许同一线程多次锁定同一个互斥锁。
1 | std::recursive_mutex rec_mtx; |
std::timed_mutex
带超时功能的互斥锁。
成员函数
try_lock_for(const chrono::duration<Rep, Period>& timeout_duration):尝试获取互斥锁,如果互斥锁已经被其他线程锁定,则在指定时间内阻塞当前线程,直到互斥锁被释放或超时,成功获取锁返回true,超时返回false。try_lock_until(const chrono::time_point<chrono::system_clock, chrono::duration<Rep, Period>>& timeout_time):尝试获取互斥锁,如果互斥锁已经被其他线程锁定,则在时间点之前阻塞当前线程,直到互斥锁被释放或超时,成功获取锁返回true,超时返回false。
std::lock_guard
RAII 风格的锁管理器,构造时锁定,析构时自动解锁。
1 | std::mutex mtx; |
std::unique_lock
更灵活的锁管理器,可以选择手动锁定和解锁,也可以选择超时时间。
成员函数
lock():手动锁定互斥锁。try_lock():尝试手动锁定互斥锁,如果互斥锁已经被其他线程锁定,则立即返回false,否则获取锁并返回true。unlock():手动解锁互斥锁。release():释放所有权,不解锁。defer_lock:创建但不锁定互斥锁。adopt_lock:接管已加锁的互斥量,避免重复加锁。
1 | std::mutex mtx1, mtx2; |
condition_variable 条件变量类
- 允许一个或多个线程等待某个条件成立。
- 其他线程可以通过
notify_one()或notify_all()来唤醒等待的线程。 - 等待线程会自动释放锁,进入阻塞状态,直到被唤醒并重新获得锁。
成员函数
wait(lock):使当前线程阻塞,直到被通知。wait(lock,pred):使当前线程阻塞,直到被通知且pred()返回true。notify_one():通知一个等待线程。notify_all():通知所有等待线程。wait_for():等待指定时间,直到被通知且条件满足或者超时,条件满足返回true,超时返回false。wait_until():等待到指定时间点,直到被通知且条件满足或者超时,条件满足返回true,超时返回false。
1 | std::mutex mtx; |
atomic 原子操作类
用于在多线程环境中执行无锁的原子操作,从而避免数据竞争并提升性能。
构造函数
1 | std::atomic<int> counter(0); |
成员函数
load():获取原子变量的值。store():设置原子变量的值。fetch_add():将原子变量的值加上指定值,并返回原值。fetch_sub():将原子变量的值减去指定值,并返回原值。exchange():交换值并返回原值。compare_exchange_strong(expected, desired):如果当前值等于expected,则将原子变量的值设置为desired,并返回true;否则,返回false。compare_exchange_weak(expected, desired):基本同compare_exchange_strong。
`strong` 和 `weak` 的区别
某些平台的硬件指令(如 ARM)在实现 CAS 时可能会偶尔失败,即使值匹配。
compare_exchange_strong 不允许虚假失败;但 compare_exchange_weak 允许,即值相同也可能失败,但同时性能更高。
所以 compare_exchange_strong 比较适合用于确保原子操作的成功,而 compare_exchange_weak 适合用于提升性能(在循环中使用)。
内存序
在多线程程序中,编译器和 CPU 为了优化性能,可能会对指令进行重排,这意味着你写在前面的代码,可能在执行时被放到后面,或者被其他线程看到的顺序不同。
内存序(memoryorder)就是用来控制这种重排行为的机制,确保线程之间的操作顺序符合预期。
| 内存序 | 同步相关 | 重拍相关 | 应用场景 |
|---|---|---|---|
memory_order_relaxed | 不同步 | 允许重排 | 高性能计数器、无依赖场景 |
memory_order_acquire | 同步之前写入 | 禁止后面的重排到前面 | 读取标志位后读取数据 |
memory_order_release | 同步之后读取 | 禁止前面的重排到后面 | 写入数据后设置标志位 |
memory_order_acq_rel | 同步前后 | 双向禁止重排 | 读写结合的同步点 |
memory_order_seq_cst | 同步所有线程 | 全局顺序一致 | 默认,最安全但性能差 |
async 异步操作类
标准库引入了一整套用于 异步操作 的类和机制,使得多线程编程更加现代化和易用。
std::async
异步执行函数,返回 std::future。
构造函数
std::launch::async:立即在新线程中执行。std::launch::deferred:延迟执行,直到std::future::get()被调用。
1 | std::future<T> f = std::async(std::launch::deferred, func, arg1, arg2); |
std::future
提供对异步操作结果的访问,表示一个尚未完成的异步操作的结果。
成员函数
get():等待异步操作完成,并返回结果。wait():等待异步操作完成。wait_for():等待指定时间,直到异步操作完成或超时。valid():检查异步操作是否有效。get_future():从promise或packaged_task获取std::future对象。
std::promise
存储值或异常,供 std::future 读取。
1 | void worker(std::promise<int> prom) { |
std::packaged_task
将可调用对象包装为异步任务。
1 | std::packaged_task<int(int,int)> task(compute); |
实用函数
sleep_for():使当前线程睡眠指定时间。sleep_until():使当前线程睡眠到指定时间点。yield():让出当前线程的执行权。
