锁(`mutex`)是 C++ 中用于保护共享资源的线程同步工具,防止多个线程同时访问同一资源而导致数据竞争(data race)或不一致的问题。`mutex` 是 "mutual exclusion" 的缩写,意为互斥锁。它的核心功能是确保同一时间只有一个线程可以访问被保护的资源。
基本概念
- 锁的加锁:当一个线程需要访问共享资源时,它需要先锁定(
lock
)对应的mutex
。加锁成功后,其他线程将无法再获得这把锁,必须等待锁被释放。 - 锁的解锁:访问完共享资源后,线程需要解锁(
unlock
)mutex
,释放锁以允许其他线程访问资源。
常用类型
-
std::mutex
最常用的互斥锁,支持显式的lock()
和unlock()
方法。 -
std::timed_mutex
支持定时等待的互斥锁,可以使用try_lock_for
和try_lock_until
来尝试在一段时间内获取锁。 -
std::recursive_mutex
支持同一线程多次加锁,但需要确保每次加锁都要有对应的解锁。 -
std::shared_mutex(C++17 引入)
提供共享锁和独占锁的能力,适用于读多写少的场景。 -
std::unique_lock 和 std::lock_guard
这两个是管理mutex
的 RAII 工具,能自动加锁和解锁,减少忘记解锁的风险。
在基础功能之上,C++ 中的其他锁类(如 std::recursive_mutex
, std::timed_mutex
, std::shared_mutex
等)主要在以下三个方面进行了扩展:
1. 锁的重入性(Reentrancy): 适合递归函数需要加锁的情况
-
扩展说明:支持同一线程多次加锁,这是由
std::recursive_mutex
提供的功能。- 如果线程 A 已经持有锁,再次尝试获取同一个锁不会导致死锁,计数器会递增。
- 每次加锁都需要匹配对应的解锁,计数器归零时锁才会被释放。
-
示例代码:
#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex r_mtx;
void recursive_function(int count) {
if (count <= 0) return;
r_mtx.lock();
std::cout << "Lock acquired in recursive call " << count << std::endl;
recursive_function(count - 1);
r_mtx.unlock();
}
int main() {
std::thread t1(recursive_function, 5);
t1.join();
return 0;
}
若同一线程需要反复访问受保护(protected)
的资源,也同样适用
2. 定时锁(Timed Locking):字面意思,避免线程长时间阻塞
-
扩展说明:提供超时机制,允许线程在一定时间内尝试获取锁,由
std::timed_mutex
和std::shared_timed_mutex
提供。try_lock_for
:指定持续的超时时间。try_lock_until
:指定绝对时间点。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::timed_mutex t_mtx;
void try_timed_lock() {
if (t_mtx.try_lock_for(std::chrono::milliseconds(100))) {
std::cout << "Lock acquired by thread " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(200));
t_mtx.unlock();
} else {
std::cout << "Thread " << std::this_thread::get_id() << " could not acquire lock." << std::endl;
}
}
int main() {
std::thread t1(try_timed_lock);
std::thread t2(try_timed_lock);
t1.join();
t2.join();
return 0;
}
3. 共享锁(Shared Locking): 允许多个线程同时读取
-
扩展说明:支持读写锁功能,由
std::shared_mutex
提供。- 独占锁(
unique_lock
):用于写操作,只有一个线程可以获得。 - 共享锁(
shared_lock
):用于读操作,多个线程可以同时持有共享锁。
- 独占锁(
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <mutex>
#include <vector>
std::shared_mutex s_mtx; // 用于保护 shared_data 的共享锁
std::mutex cout_mtx; // 用于保护 std::cout 的独占锁
int shared_data = 0;
void writer() {
int input_value;
std::cout << "Writer thread " << std::this_thread::get_id() << ", please enter a value: ";
std::cin >> input_value;
std::unique_lock<std::shared_mutex> lock(s_mtx); // 独占锁
shared_data = input_value;
{
std::lock_guard<std::mutex> cout_lock(cout_mtx); // 保护 std::cout
std::cout << "Writer thread " << std::this_thread::get_id()
<< " updated shared_data to " << shared_data << std::endl;
}
}
void reader(int id) {
std::shared_lock<std::shared_mutex> lock(s_mtx); // 共享锁
{
std::lock_guard<std::mutex> cout_lock(cout_mtx); // 保护 std::cout
std::cout << "Reader " << id << " (Thread " << std::this_thread::get_id()
<< ") reads shared_data: " << shared_data << std::endl;
}
}
int main() {
std::thread t1(writer);
// 启动多个 reader 线程
std::vector<std::thread> readers;
for (int i = 1; i <= 3; ++i) {
readers.emplace_back(reader, i);
}
t1.join();
for (auto& t : readers) {
t.join();
}
return 0;
}
有的读者可能会注意到一些问题:reader和writer共用一个smtx
为什么需要共用一个锁?
-
共享资源的一致性:
writer
修改共享数据时,reader
必须等到writer
完成修改后才能读取,确保读取到的数据是最新的。reader
并发访问时,多个线程同时读取不会改变数据,因此可以安全地共享锁,而无需相互等待。
-
避免竞争条件:
- 如果没有锁保护,
writer
和reader
可能同时访问shared_data
,导致以下问题:reader
在writer
写入未完成时读取了不完整的数据(数据竞争)。writer
在多个reader
同时访问时强行写入,导致未定义行为。
- 如果没有锁保护,
-
锁的类型支持读多写少的场景:
- 使用
std::shared_mutex
,允许:- 多个
reader
同时持有 共享锁。 - 一个
writer
独占持有 独占锁,阻止其他reader
或writer
。
- 多个
- 这种机制适合读多写少的场景,比如配置文件读取、缓存系统等。
- 使用
但是你会得到如下结果:reader在writer前先行执行,导致读入值为0
![[Pasted image 20250116173101.png]]
问题分析
-
线程调度的不确定性:
reader
和writer
是并发执行的,具体哪个线程先运行是由系统线程调度决定的。你现在的输出表明:reader
线程在writer
完成写入之前就读取了shared_data
,因此读取到的是初始值0
。
-
std::shared_mutex
的保护范围:- 目前
std::shared_mutex
确保了单个线程对shared_data
的操作是安全的,但无法保证reader
等待writer
完成更新后再开始读取。 - 换句话说,锁本身无法提供线程之间的执行顺序。
- 目前
解决方法:std::condition_variable
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <mutex>
#include <condition_variable>
#include <vector>
std::shared_mutex s_mtx; // 用于保护 shared_data 的共享锁
std::mutex cv_mtx; // 用于保护条件变量
std::condition_variable cv; // 条件变量
bool data_ready = false; // 标志共享数据是否更新
int shared_data = 0;
void writer() {
int input_value;
std::cout << "Writer thread " << std::this_thread::get_id() << ", please enter a value: ";
std::cin >> input_value;
{
std::unique_lock<std::shared_mutex> lock(s_mtx); // 独占锁
shared_data = input_value;
}
{
std::lock_guard<std::mutex> lock(cv_mtx); // 保护条件变量
data_ready = true; // 标志数据已更新
}
cv.notify_all(); // 通知所有等待线程
std::cout << "Writer thread " << std::this_thread::get_id()
<< " updated shared_data to " << shared_data << std::endl;
}
void reader(int id) {
{
std::unique_lock<std::mutex> lock(cv_mtx);
cv.wait(lock, [] { return data_ready; }); // 等待数据更新
}
std::shared_lock<std::shared_mutex> lock(s_mtx); // 共享锁读取数据
std::cout << "Reader " << id << " (Thread " << std::this_thread::get_id()
<< ") reads shared_data: " << shared_data << std::endl;
}
int main() {
std::thread t1(writer);
// 启动多个 reader 线程
std::vector<std::thread> readers;
for (int i = 1; i <= 3; ++i) {
readers.emplace_back(reader, i);
}
t1.join();
for (auto& t : readers) {
t.join();
}
return 0;
}
std::unique_lock
和 std::condition_variable
的配合使用
std::unique_lock
比 std::lock_guard
更适合用于与 std::condition_variable
配合使用,因为它能够在等待时释放锁,并在唤醒后自动重新获取锁。
生产者-消费者模式
工作原理
-
等待线程(消费者):
- 调用
wait
方法挂起线程,直到某个条件为真。 wait
方法需要配合锁(std::unique_lock
)使用,以便在等待时保护共享资源。
- 调用
-
通知线程(生产者):
- 调用
notify_one
或notify_all
方法,通知一个或所有等待线程重新检查条件。
- 调用
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> data_queue; // 数据队列
std::mutex mtx; // 用于保护队列的互斥锁
std::condition_variable cv; // 条件变量
bool done = false; // 标志数据是否生成完成
// 消费者线程:处理数据
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx); // 获取锁
cv.wait(lock, [] { return !data_queue.empty() || done; }); // 等待条件
while (!data_queue.empty()) {
int data = data_queue.front();
data_queue.pop();
std::cout << "Consumed: " << data << std::endl;
}
if (done) break; // 数据生成完成,退出循环
}
}
// 生产者线程:生成数据
void producer() {
for (int i = 1; i <= 5; ++i) {
{
std::lock_guard<std::mutex> lock(mtx); // 加锁
data_queue.push(i); // 生成数据
std::cout << "Produced: " << i << std::endl;
}
cv.notify_one(); // 通知消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
{
std::lock_guard<std::mutex> lock(mtx);
done = true; // 标记数据生成完成
}
cv.notify_all(); // 通知所有等待线程
}
int main() {
std::thread t1(consumer);
std::thread t2(producer);
t1.join();
t2.join();
return 0;
}