在 C++11 及以后版本中,标准库提供了一套完整的多线程同步原语,帮助程序员在多线程环境中安全地访问共享内存。下面将从概念、常用同步工具以及最佳实践三方面进行阐述,并给出实用的代码示例。
1. 同步的核心概念
| 术语 | 含义 |
|---|---|
| 临界区 | 访问共享资源的代码段 |
互斥量 (mutex) |
用于保证同一时刻只有一个线程进入临界区 |
条件变量 (condition_variable) |
用于线程之间的等待与通知 |
原子类型 (std::atomic) |
对基本类型提供无锁的原子操作 |
读写锁 (std::shared_mutex) |
允许多个读者并发、但写者独占 |
理解这些概念后,才能正确选择合适的同步工具。
2. 互斥量与 lock_guard
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex g_mutex;
int g_counter = 0;
void increment(int id, int times) {
for (int i = 0; i < times; ++i) {
std::lock_guard<std::mutex> lock(g_mutex); // RAII 自动上锁/解锁
++g_counter;
std::cout << "Thread " << id << " incremented counter to " << g_counter << '\n';
}
}
int main() {
std::vector<std::thread> workers;
for (int i = 0; i < 4; ++i) {
workers.emplace_back(increment, i, 5);
}
for (auto &t : workers) t.join();
std::cout << "Final counter: " << g_counter << '\n';
}
要点
std::lock_guard采用 RAII,异常安全。- 只在需要临界区时使用,避免锁持有时间过长。
3. 原子操作
对于单一整数、布尔值等基本类型,使用 std::atomic 能避免锁开销。
#include <atomic>
#include <thread>
std::atomic <int> atom_counter{0};
void add_to_atomic(int times) {
for (int i = 0; i < times; ++i) {
atom_counter.fetch_add(1, std::memory_order_relaxed);
}
}
使用建议
- 只对独立变量使用原子操作。
- 对复杂数据结构(如链表、队列)仍需使用互斥量或锁-free 设计。
4. 条件变量
当线程需要等待某个状态变化时,可使用 std::condition_variable。
#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex q_mutex;
std::condition_variable q_cond;
std::queue <int> q;
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(q_mutex);
q.push(i);
std::cout << "Produced: " << i << '\n';
}
q_cond.notify_one(); // 唤醒消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(q_mutex);
q_cond.wait(lock, []{ return !q.empty(); }); // 等待队列非空
int val = q.front();
q.pop();
lock.unlock(); // 释放锁后再处理
std::cout << "Consumed: " << val << '\n';
if (val == 9) break; // 结束条件
}
}
最佳实践
wait的谓词函数(lambda)确保重入安全。- 在唤醒后立即解锁再进行耗时操作,减少锁竞争。
5. 读写锁 (std::shared_mutex)
当读操作远多于写操作时,可使用共享锁:
#include <shared_mutex>
std::shared_mutex g_sharedMutex;
std::unordered_map<int, std::string> g_cache;
void writer(int key, const std::string &value) {
std::unique_lock<std::shared_mutex> lock(g_sharedMutex); // 写者独占
g_cache[key] = value;
}
std::string reader(int key) {
std::shared_lock<std::shared_mutex> lock(g_sharedMutex); // 读者共享
auto it = g_cache.find(key);
return it != g_cache.end() ? it->second : "";
}
注意
- 读者锁不互斥,写者锁会阻塞所有读者和写者。
- 读写锁在极端写多场景下并不合适。
6. 最佳实践小结
| 场景 | 推荐同步工具 | 说明 |
|---|---|---|
| 简单计数器、布尔标记 | std::atomic |
无锁,性能最高 |
访问共享容器(如 std::vector, std::map) |
std::mutex + std::lock_guard |
简单易用 |
| 生产者-消费者 | std::condition_variable |
等待/通知 |
| 大量读少量写 | std::shared_mutex |
读者共享 |
| 需要手动解锁 | std::unique_lock |
可自定义锁释放时间 |
另外,始终遵循 最小化锁粒度、锁的持有时间尽量短、避免死锁(如统一锁顺序、使用 try_lock) 的原则,才能写出既安全又高效的多线程 C++ 代码。
7. 结语
C++ 标准库为多线程同步提供了丰富而强大的工具。通过合理组合 mutex, atomic, condition_variable, shared_mutex 等原语,并遵循上述最佳实践,开发者可以在保持代码可读性的同时,避免常见的并发错误,从而构建高性能、可维护的并发程序。祝编码愉快!