在 C++17 之前,多线程同步主要依赖于 std::mutex、std::recursive_mutex、std::timed_mutex 等互斥锁类型。随着 C++20 的到来,标准库进一步扩展了同步原语,如 std::shared_mutex、std::shared_lock 等,用于实现更细粒度的读写同步。本文将从基本概念出发,逐步剖析不同同步工具的适用场景、实现细节以及性能注意事项。
1. 互斥锁(Mutex)
1.1 基础用法
#include <mutex>
#include <thread>
std::mutex mtx;
int counter = 0;
void increment()
{
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
std::lock_guard 是一种 RAII 风格的互斥锁包装器,构造时锁定,析构时解锁。它的优势在于异常安全:无论 increment 里出现何种异常,锁都会被正确释放。
1.2 递归互斥锁
std::recursive_mutex rmtx;
void recursive_function(int depth)
{
std::lock_guard<std::recursive_mutex> lock(rmtx);
if (depth > 0) recursive_function(depth - 1);
}
递归互斥锁允许同一线程多次锁定同一把锁,但实现代价更高,使用时需谨慎。
1.3 计时互斥锁
std::timed_mutex tmtx;
if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
// 成功获取锁
tmtx.unlock();
}
计时互斥锁支持超时机制,适合需要避免死锁的场景。
2. 读写锁(Shared Mutex)
在读多写少的场景下,使用 std::shared_mutex 可以显著提升并发性能。读者可同时获取共享锁,而写者必须独占锁。
#include <shared_mutex>
#include <vector>
std::shared_mutex rw_mutex;
std::vector <int> data;
void reader()
{
std::shared_lock<std::shared_mutex> lock(rw_mutex);
// 只读访问
auto sum = std::accumulate(data.begin(), data.end(), 0);
}
void writer()
{
std::unique_lock<std::shared_mutex> lock(rw_mutex);
// 写操作
data.push_back(1);
}
2.1 读写锁的竞争模型
- 读者占用锁:只需要共享锁即可,不会阻塞其他读者。
- 写者占用锁:独占锁会阻塞所有正在进行的读写操作,直到写者完成。
若读写比例极度偏向写,使用读写锁可能导致写者饥饿。为此,C++20 引入了 std::shared_timed_mutex,提供更灵活的等待策略。
3. 原子操作(Atomic)
C++ 标准库提供 std::atomic 用于实现无锁并发。其主要优势是原子性与轻量级。
#include <atomic>
std::atomic <int> atomic_counter{0};
void fast_increment()
{
atomic_counter.fetch_add(1, std::memory_order_relaxed);
}
memory_order_relaxed适用于不需要同步其他内存操作的情况。- 对于更严格的同步需求,可使用
memory_order_acquire/memory_order_release等。
4. 条件变量(Condition Variable)
条件变量用于线程间的信号机制,常与互斥锁配合使用。
#include <condition_variable>
std::mutex cv_mtx;
std::condition_variable cv;
bool ready = false;
void waiter()
{
std::unique_lock<std::mutex> lock(cv_mtx);
cv.wait(lock, []{ return ready; });
// 继续执行
}
void notifier()
{
{
std::lock_guard<std::mutex> lock(cv_mtx);
ready = true;
}
cv.notify_one(); // 或 notify_all()
}
wait 的谓词形式可避免虚假唤醒。
5. 现代 C++ 并发编程技巧
| 技巧 | 说明 | 示例 |
|---|---|---|
std::scoped_lock |
同时锁定多个互斥锁,避免死锁 | std::scoped_lock lock(m1, m2); |
std::async |
异步执行返回 std::future |
auto fut = std::async(std::launch::async, func); |
std::latch / std::barrier |
C++20 提供的同步原语 | std::latch l(3); |
6. 性能注意事项
- 锁粒度:过细的锁会导致频繁上下文切换,过粗的锁会降低并发度。建议按功能拆分最小共享区域。
- 锁竞争:通过
std::atomic或无锁设计减少锁竞争,特别是在高并发读写场景。 - 缓存行对齐:避免“false sharing”,可使用
alignas(64)或[[no_unique_address]]。 - 优先级反转:在实时系统中需使用带优先级继承的互斥锁。
7. 结语
C++ 标准库为多线程同步提供了丰富且易用的原语。掌握互斥锁、读写锁、原子操作和条件变量的使用原则,并结合现代 C++ 的 RAII、std::scoped_lock 等特性,可写出既安全又高效的并发代码。在实际项目中,合理评估线程数、锁粒度与竞争情况,往往是性能优化的关键。祝你编码愉快!