C++ 并发编程入门
并发编程是指在多线程或多进程环境下,协调多个执行流对共享资源的访问,以避免数据竞争(data race)和状态不一致。在 C++ 中,我们通常使用互斥锁(std::mutex)来保护临界区,确保同一时刻只有一个线程能访问共享资源。
std::mutex 与 RAII 锁管理
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
#include <iostream>
#include <thread>
#include <mutex>
int number = 0;
std::mutex _mutex;
void number_adder(const int& value) {
// 推荐:使用 lock_guard 自动管理锁(RAII)
std::lock_guard lock(_mutex);
number += value;
// lock_guard 析构时自动解锁,避免死锁
}
int main() {
int value = 10;
std::thread t1(number_adder, std::ref(value));
std::thread t2(number_adder, std::ref(value));
t1.join();
t2.join();
std::cout << "number = " << number << std::endl; // 输出: 20
return 0;
}
在这个例子中,两个线程同时对全局变量 number 执行加法操作。若无锁保护,可能出现以下竞态条件:
- 线程 t1 读取
number = 0 - 线程 t2 也读取
number = 0 - 两者分别计算
0 + 10 = 10 - 先后写回,最终结果为
10而非预期的20
通过互斥锁保护,确保加法操作的原子性,从而得到正确结果。
std::condition_variable:条件等待与协作
std::mutex 解决了“互斥访问”,但无法处理“条件不满足时需等待”的场景。例如:消费者线程希望从队列中取数据,但队列为空——此时不应忙等待,而应释放锁并阻塞,直到生产者放入新数据。
为此,C++ 提供了 std::condition_variable,配合 std::unique_lock 实现高效等待。
线程安全队列示例
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
#include <queue>
#include <mutex>
#include <condition_variable>
template
class ThreadSafeQueue {
private:
std::queue _m_queue;
mutable std::mutex _m_mutex;
std::condition_variable _m_cond;
public:
ThreadSafeQueue() = default;
ThreadSafeQueue(const ThreadSafeQueue&) = delete;
ThreadSafeQueue& operator=(const ThreadSafeQueue&) = delete;
void push(const T& val) {
{
std::lock_guard lock(_m_mutex);
_m_queue.push(val);
} // 自动解锁
_m_cond.notify_one(); // 唤醒一个等待的消费者
}
void pop(T& val) {
std::unique_lock lock(_m_mutex);
// wait 会:1) 检查谓词;2) 若为 false,则原子地释放锁并阻塞
// 3) 被唤醒后重新获取锁,再次检查谓词
_m_cond.wait(lock, [this] { return !_m_queue.empty(); });
val = std::move(_m_queue.front());
_m_queue.pop();
}
};
关键点解析
为什么
pop必须用unique_lock而不是lock_guard?
因为condition_variable::wait()需要在等待时临时释放锁,并在被唤醒后自动重新加锁。lock_guard不支持手动解锁,而unique_lock支持,因此是唯一合法选择。wait的谓词(predicate)为何必要?
防止“虚假唤醒”(spurious wakeup)——即使没有调用notify,系统也可能唤醒等待线程。通过循环检查条件(!_m_queue.empty()),确保只有条件真正满足时才继续执行。notify_one()vsnotify_all()
此处只需唤醒一个消费者,故用notify_one()更高效。若多个线程等待不同条件,则需notify_all()。
总结
std::mutex:提供互斥访问,防止数据竞争。std::lock_guard:基于 RAII 的自动锁管理,适用于简单临界区。std::unique_lock+std::condition_variable:用于“条件等待”场景,支持在等待时释放锁,避免死锁和资源浪费。
现代多核设备普遍支持并发执行。对于 I/O 密集型任务(如网络请求、文件读写),多线程可将计算与I/O 等待解耦:主线程提交 I/O 任务后立即处理下一工作,无需阻塞等待,从而显著提升系统吞吐量与响应性。
并发编程虽强大,但也引入了复杂性。始终遵循:最小化临界区、避免嵌套锁、优先使用 RAII、善用条件变量,才能写出高效且安全的并发代码。