cpp中的锁

mutex基础用法

创建std::mutex:

1
std::mutex mtx_;

对mutex加锁:

1
mtx_.lock();

对mutex解锁:

1
mtx_.unlock();

以上是 std::mutex 的基础用法,需要显式地调用 lock() 和 unlock() 来加锁和解锁。这种方式容易因异常或提前返回而导致忘记解锁。为了避免这种风险,可以使用基于 RAII(资源获取即初始化)思想的锁对象,在创建时自动加锁,并在离开作用域时自动解锁,从而确保锁的正确释放。

使用std::lock_guard

std::lock_guard 是最简单的 RAII 锁封装,构造时自动上锁,析构时自动解锁:

1
2
3
4
5
6
std::mutex mtx;

void safe_print() {
std::lock_guard<std::mutex> lock(mtx);
std::cout << "thread-safe printing\n";
} // 离开作用域时自动解锁

推荐在函数作用域内使用

使用std::unique_lock

std::unique_lock 功能更强大,支持延迟上锁、手动解锁、重新加锁、条件变量等。如果没有解锁,则在离开锁的作用域时会自动解锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::mutex mtx;

void example() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟上锁,先创建,不立即加锁
// 执行其他准备操作

lock.lock(); // 手动加锁
// 临界区

lock.unlock(); // 手动解锁

lock.lock(); // 重新上锁
// 临界区
}

std::unique_lock 是 std::condition_variable 唯一支持的锁类型。
这是它最重要的用途之一:允许线程等待条件满足时再继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker() {
std::unique_lock<std::mutex> lock(mtx);
std::cout << "工作线程开始等待\n";
cv.wait(lock, [] { return ready; }); // 等待条件满足
std::cout << "工作线程开始执行\n";
}

void notifier() {
std::this_thread::sleep_for(std::chrono::seconds(3));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 唤醒等待线程
}

使用std::scoped_lock

std::scoped_lock 是 cpp17 引入的一种更现代的锁管理工具,用于简化多互斥量(std::mutex)的加锁操作,并且可以自动避免死锁。

单个锁:

1
2
3
4
5
6
std::mutex mtx;

void example() {
std::scoped_lock lock(mtx);
// 临界区
} // 离开作用域时自动解锁

多个锁(自动避免死锁)

1
2
3
4
5
6
7
8
9
void f1() {
std::scoped_lock lock(m1, m2); // 一次性安全加锁两个互斥量
std::cout << "线程1获取 m1, m2\n";
}

void f2() {
std::scoped_lock lock(m1, m2); // 即使顺序不同也不会死锁
std::cout << "线程2获取 m1, m2\n";
}

std::scoped_lock 会自动避免死锁

死锁通常发生在多个线程同时试图获取多个互斥锁,且加锁顺序不一致的情况下:

1
2
3
4
5
6
7
8
9
10
11
std::mutex m1, m2;

void ThreadA() {
std::lock_guard<std::mutex> lock1(m1);
std::lock_guard<std::mutex> lock2(m2);
}

void ThreadB() {
std::lock_guard<std::mutex> lock1(m2);
std::lock_guard<std::mutex> lock2(m1);
}

std::scoped_lock 底层实现是基于 std::lock(),而 std::lock() 是一个专门为避免死锁设计的函数。std::lock() 打破了不可剥夺条件,采用“全拿全放”策略,核心思想是尝试性加锁 + 回退重试。

[!WARNING]
注意:条件变量必须使用 std::unique_lock,不能用 std::scoped_lock。

读写锁

cpp17引入了 std::shared_mutex 和 std::shared_lock 来实现“读写锁”机制。包含于头文件<shared_mutex>中。

写线程(独占锁):

写线程必须使用 std::unique_lock 或 std::lock_guard 或 std::scoped_lock 来加独占锁:

1
2
3
4
5
6
7
8
9
10
11
12
13

#include <shared_mutex>
#include <thread>
#include <iostream>

std::shared_mutex smtx;
int counter = 0;

void writer() {
std::unique_lock<std::shared_mutex> lock(smtx); // 写锁(独占)
++counter;
std::cout << "写线程修改 counter = " << counter << "\n";
}

读线程(共享锁):

读线程可以使用 std::shared_lock 来加共享锁:

1
2
3
4
void reader(int id) {
std::shared_lock<std::shared_mutex> lock(smtx); // 读锁(共享)
std::cout << "读线程 " << id << " 读取 counter = " << counter << "\n";
}