同步机制
同步机制让多个线程以正确的顺序访问共享状态。选择原则很简单:一个独立的简单状态可考虑原子对象;要维护多个数据或多个步骤之间的不变量时,优先使用互斥锁保护临界区。
选择同步原语
| 工具 |
解决的问题 |
不能替代什么 |
std::mutex + RAII 锁 |
互斥访问复杂共享状态 |
事件通知与任务结果传递 |
std::condition_variable |
等待受互斥锁保护的条件变化 |
对条件本身的互斥保护 |
std::atomic<T> |
对单个原子状态进行读改写与同步 |
多对象事务或复杂容器保护 |
std::scoped_lock |
一次获取多把锁,降低死锁风险 |
设计良好的锁层级 |
原子操作不一定是无锁(可用 is_lock_free() 查询),无锁也不等于无等待。不要仅因“可能更快”就把清晰的锁方案改为 CAS 循环。
互斥锁与 RAII
std::mutex 的 lock()/unlock() 成功配对可建立同步关系。手写 unlock() 容易在提前返回或异常时遗漏,因此应让 RAII 对象持有锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <iostream> #include <mutex> #include <thread>
std::mutex counter_mutex; int counter = 0;
void add() { for (int i = 0; i < 100'000; ++i) { std::lock_guard<std::mutex> lock(counter_mutex); ++counter; } }
int main() { std::thread first(add); std::thread second(add); first.join(); second.join(); std::cout << counter << '\n'; }
|
lock_guard 适合“进入作用域即加锁、离开即解锁”。需要延迟加锁、临时解锁或交给条件变量等待时,使用 std::unique_lock<std::mutex>。
1 2 3 4 5
| std::unique_lock<std::mutex> lock(counter_mutex, std::defer_lock);
lock.lock();
lock.unlock();
|
unique_lock 的灵活性有额外状态和误用空间;不需要它的功能时选 lock_guard。
条件变量:等待“谓词”,不是等待通知
条件变量适合“队列非空”“已停止”等状态发生改变的场景。通知可能发生在等待前,线程也可能虚假唤醒;所以条件状态必须由同一把互斥锁保护,并且总是用谓词判断。
1 2 3
| 生产者:持锁修改队列/停止标志 → 解锁 → notify_one 或 notify_all 消费者:持锁检查谓词 → 不满足则 wait(原子地解锁并睡眠) 被唤醒后重新加锁并再次检查谓词
|
有关闭协议的生产者消费者示例
下面是可独立编译的 C++17 示例。队列容量上限提供背压;closed 让消费者在生产结束且队列清空后退出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| #include <condition_variable> #include <cstddef> #include <iostream> #include <mutex> #include <optional> #include <queue> #include <thread>
class BlockingQueue { public: explicit BlockingQueue(std::size_t capacity) : capacity_(capacity) {}
bool push(int value) { std::unique_lock<std::mutex> lock(mutex_); not_full_.wait(lock, [this] { return closed_ || queue_.size() < capacity_; }); if (closed_) { return false; } queue_.push(value); lock.unlock(); not_empty_.notify_one(); return true; }
std::optional<int> pop() { std::unique_lock<std::mutex> lock(mutex_); not_empty_.wait(lock, [this] { return closed_ || !queue_.empty(); }); if (queue_.empty()) { return std::nullopt; } const int value = queue_.front(); queue_.pop(); lock.unlock(); not_full_.notify_one(); return value; }
void close() { { std::lock_guard<std::mutex> lock(mutex_); closed_ = true; } not_empty_.notify_all(); not_full_.notify_all(); }
private: const std::size_t capacity_; std::mutex mutex_; std::condition_variable not_empty_; std::condition_variable not_full_; std::queue<int> queue_; bool closed_ = false; };
int main() { BlockingQueue queue(2); std::thread producer([&queue] { for (int value = 1; value <= 5; ++value) { if (!queue.push(value)) return; } queue.close(); }); std::thread consumer([&queue] { while (const auto value = queue.pop()) { std::cout << "消费: " << *value << '\n'; } }); producer.join(); consumer.join(); }
|
wait(lock, predicate) 等价于在循环中检查谓词、条件不满足则等待。notify 不携带数据,也不“记住”事件;真正决定能否继续的是锁保护的状态。
原子对象
std::atomic<T> 支持对 T 的原子读、写和部分读改写。下例只有一个计数器不变量,可用 fetch_add;默认的顺序一致性适合先保证正确性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <atomic> #include <iostream> #include <thread>
std::atomic<int> counter{0};
void add() { for (int i = 0; i < 100'000; ++i) { counter.fetch_add(1); } }
int main() { std::thread first(add), second(add); first.join(); second.join(); std::cout << counter.load() << '\n'; }
|
memory_order_relaxed 只适用于只需原子性、无需借此发布其他数据的场景,例如独立统计计数。发布数据或实现协议前,应先理解 acquire/release;详见CAS:内存序。
死锁与多把锁
死锁通常需要互斥、持有并等待、不可抢占与循环等待四个条件同时存在。不要通过超时“掩盖”锁设计问题;优先减少嵌套锁,并为必须同时获取的锁建立统一策略。
1 2 3 4 5 6 7 8 9
| #include <mutex>
std::mutex account_a_mutex; std::mutex account_b_mutex;
void transfer() { std::scoped_lock lock(account_a_mutex, account_b_mutex); }
|
std::scoped_lock 会采用避免死锁的获取策略,并在析构时释放所有锁。它不能保护其他地方以不同规则持锁的设计;整个代码库仍应有明确锁的所有权与顺序。
std::recursive_mutex 允许同一线程重复加锁,但会隐藏可重入设计和锁粒度问题,通常应重构为“不在持锁状态调用可能回调自身的函数”。std::timed_mutex 的超时只表示本次未获锁,不等于消除了死锁或饥饿。
常见误区与实践建议
- 不要在持锁状态执行 I/O、耗时计算或未知回调;必要时复制最小数据后解锁再处理。
- 条件变量的停止标志与队列状态必须同锁保护,停止时要唤醒所有可能等待的线程。
volatile 不是线程同步工具,不能替代 atomic 或 mutex。
- 先使用默认内存序和清晰的锁;只在测量并证明需要时优化内存序或无锁算法。
- C++17 编译示例:
c++ -std=c++17 -Wall -Wextra -Wpedantic -pthread file.cpp。