C++11 std::mutex

C++11 std::mutex

多线程程序里最容易出问题的地方之一,就是多个线程同时读写共享数据。
std::mutex 就是 C++11 提供的最基础互斥锁,用来保护临界区。

1. 为什么需要互斥锁?

来看一个简单场景:两个线程同时给同一个计数器加一。
如果没有同步保护,结果就可能出现竞态条件,最终值未必正确。

2. 基本用法

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
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;

std::mutex mtx;
int counter = 0;

void work()
{
for (int i = 0; i < 1000; ++i) {
mtx.lock();
++counter;
mtx.unlock();
}
}

int main()
{
thread t1(work);
thread t2(work);

t1.join();
t2.join();

cout << counter << endl;
return 0;
}

3. 更推荐的写法:lock_guard

手动 lock() / unlock() 虽然能用,但不够稳,尤其一旦中间出现异常或提前返回,就容易忘记解锁。

更推荐这样写:

1
2
3
4
5
6
7
void work()
{
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}

这就是典型的 RAII 风格。

4. 一个更完整的示例

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
#include <thread>
#include <mutex>
#include <vector>
#include <iostream>
using namespace std;

mutex mtx;
int total = 0;

void addTask()
{
for (int i = 0; i < 10000; ++i) {
lock_guard<mutex> lock(mtx);
++total;
}
}

int main()
{
vector<thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(addTask);
}

for (auto& t : threads) {
t.join();
}

cout << total << endl;
return 0;
}

5. 使用时要注意的点

5.1 锁的范围尽量小

锁持有时间越长,线程阻塞越明显。
所以通常应只把真正需要保护的代码放进临界区。

5.2 避免重复加锁导致死锁

如果同一线程在不合适的场景下重复拿同一把锁,程序可能会卡死。

5.3 不要忘记 join 线程

锁解决的是共享数据同步问题,线程本身的生命周期管理还是要自己处理好。

总结

std::mutex 是 C++11 并发编程里最基础的一把锁。
它不复杂,但非常重要。实际开发里,通常会配合 std::lock_guardstd::unique_lock 使用,这样比手动加解锁更安全。