同步机制用于协调多个线程堆共享资源的访问。

互斥锁mutex

std::mutex是C++中最基础的互斥锁,需要引入头文件<mutex>,它保证同一时刻只能有一个线程能够进入临界区。

示例:

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 counter = 0;
std::mutex mtx;

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

int main() {
std::thread t1(add);
std::thread t2(add);

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

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

此时结果稳定为:

1
200000

但是手动调用 lock()unlock() 有风险。

如果临界区中发生异常,可能导致锁无法释放。

lock_guard

std::lock_guard是 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
#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex mtx;

void add() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}

int main() {
std::thread t1(add);
std::thread t2(add);

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

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

优点:

  • 自动释放锁
  • 异常安全
  • 写法简洁

unique_lock

std::unique_lockstd::lock_guard更灵活。

它支持:

  • 延迟加锁
  • 手动解锁
  • 转移所有权
  • 可配合条件变量使用

示例:

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>

std::mutex mtx;

void task() {
std::unique_lock<std::mutex> lock(mtx);

std::cout << "线程正在执行临界区代码" << std::endl;

lock.unlock();

std::cout << "线程执行非临界区代码" << std::endl;
}

int main() {
std::thread t1(task);
std::thread t2(task);

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

return 0;
}

recursive_mutex

std::recursive_mutex允许同一个线程多次获得同一把锁,普通std::mutex如果同一个线程重复加锁,会导致锁死。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <mutex>

std::recursive_mutex rmtx;

void func(int n) {
if (n <= 0) return;

std::lock_guard<std::recursive_mutex> lock(rmtx);
std::cout << "n = " << n << std::endl;

func(n - 1);
}

int main() {
func(3);
return 0;
}

注意:

虽然 recursive_mutex 可以避免递归调用时死锁,但不应滥用。

通常更推荐重新设计代码结构。

timed_mutex

std::timed_mutex支持尝试在一定时间内加锁。

示例:

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
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::timed_mutex tmtx;

void task(int id) {
if (tmtx.try_lock_for(std::chrono::seconds(1))) {
std::cout << "线程 " << id << " 获得锁" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
tmtx.unlock();
} else {
std::cout << "线程 " << id << " 获取锁超时" << std::endl;
}
}

int main() {
std::thread t1(task, 1);
std::thread t2(task, 2);

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

return 0;
}

条件变量 condition_variable

条件变量用于线程之间的等待和通知。

常用于:

  • 生产者消费者模型
  • 任务队列
  • 线程池
  • 等待某个状态变化

基本函数

1
std::condition_variable cv;

常用操作:

1
2
3
4
cv.wait(lock);
cv.wait(lock, predicate);
cv.notify_one();
cv.notify_all();

生产者消费者示例

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
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;

void producer() {
for (int i = 1; i <= 5; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
q.push(i);
std::cout << "生产: " << i << std::endl;
}

cv.notify_one();
}
}

void consumer() {
for (int i = 1; i <= 5; ++i) {
std::unique_lock<std::mutex> lock(mtx);

cv.wait(lock, [] {
return !q.empty();
});

int value = q.front();
q.pop();

std::cout << "消费: " << value << std::endl;
}
}

int main() {
std::thread t1(producer);
std::thread t2(consumer);

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

return 0;
}

为什么 wait 要配合条件判断?

推荐写法:

1
2
3
cv.wait(lock, [] {
return !q.empty();
});

原因:

  • 防止虚假唤醒
  • 避免条件不满足时继续执行
  • 代码更安全

原子操作 atomic

std::atomic用于无锁地操作共享变量。

适合简单的数据类型,例如:

  • 整数计数器
  • 布尔标志
  • 指针

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> counter{0};

void add() {
for (int i = 0; i < 100000; ++i) {
++counter;
}
}

int main() {
std::thread t1(add);
std::thread t2(add);

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

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

输出稳定为:

1
200000

atomic 与 mutex 的区别

对比项 atomic mutex
适用场景 简单变量操作 复杂临界区
性能 通常较高 有锁开销
使用难度 中等 简单直观
是否阻塞 通常不阻塞 可能阻塞
表达能力 较弱 较强

CAS

CAS(Compare-And-Swap / Compare-And-Exchange,比较并交换)是并发编程中的一种原子操作,常用于实现无锁数据结构、原子计数器、自旋锁等。

死锁

死锁指多个线程互相等待对方释放资源,导致程序永久阻塞。

示例:

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
#include <iostream>
#include <thread>
#include <mutex>

std::mutex m1;
std::mutex m2;

void thread1() {
std::lock_guard<std::mutex> lock1(m1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock2(m2);

std::cout << "thread1 done" << std::endl;
}

void thread2() {
std::lock_guard<std::mutex> lock1(m2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock2(m1);

std::cout << "thread2 done" << std::endl;
}

int main() {
std::thread t1(thread1);
std::thread t2(thread2);

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

return 0;
}

这里可能发生死锁:

1
2
线程 1 持有 m1,等待 m2
线程 2 持有 m2,等待 m1

避免死锁的方法

方法一:固定加锁顺序

所有线程都按相同顺序加锁。

1
2
std::lock_guard<std::mutex> lock1(m1);
std::lock_guard<std::mutex> lock2(m2);

方法二:使用 std::lock

std::lock 可以一次性锁住多个 mutex,避免死锁。

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>

std::mutex m1;
std::mutex m2;

void task() {
std::lock(m1, m2);

std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);

std::cout << "任务完成" << std::endl;
}

int main() {
std::thread t1(task);
std::thread t2(task);

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

return 0;
}

方法三:使用 scoped_lock

C++17 推荐使用 std::scoped_lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <thread>
#include <mutex>

std::mutex m1;
std::mutex m2;

void task() {
std::scoped_lock lock(m1, m2);

std::cout << "任务完成" << std::endl;
}

int main() {
std::thread t1(task);
std::thread t2(task);

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

return 0;
}