C/C++锁,从入门到躺平
1. 核心概念:为什么需要锁?
在多线程程序中,当多个线程需要访问共享数据(Shared Data) 时,如果至少有一个线程要修改(写)这个数据,就会发生数据竞争(Data Race)。数据竞争会导致未定义行为(程序崩溃、计算结果错误、诡异的问题等)。
锁(Lock) 是一种同步原语(Synchronization Primitive),用于强制保证在任意时刻,只有一个线程可以执行被锁保护的代码段(称为临界区,Critical Section),从而避免数据竞争。
核心思想:将可能导致数据竞争的“读-写”或“写-写”操作变为原子操作。
2. C++标准库中的锁家族
C++标准库在 <mutex> 头文件中提供了多种互斥量和管理类。下图清晰地展示了它们之间的关系:

2.1 互斥量类型 (Mutex Types)
这是真正的资源,负责最基础的互斥功能。
| 类型 | 说明 | 使用场景 |
|---|---|---|
std::mutex | 最基本的互斥量。不可递归上锁(同一线程重复加锁会导致死锁)。 | 最通用的场景,需要独占访问共享资源时。 |
std::recursive_mutex | 递归互斥量。允许同一线程多次加锁,解锁次数需与加锁次数相同。 | 需要从递归函数中访问共享资源,或者调用可能(再次)加锁的成员函数。性能稍差于std::mutex,应优先使用std::mutex。 |
std::timed_mutex | 带超时功能的互斥量。除了lock(),还提供了try_lock_for()和try_lock_until()。 | 需要尝试获取锁,如果获取不到不想无限期等待,而是做别的事情时。 |
std::recursive_timed_mutex | 带超时功能的递归互斥量。以上两者的结合。 | 同时需要递归和超时功能的复杂场景。 |
std::shared_mutex (C++17) | 共享互斥量。实现了读写锁(Reader-Writer Lock) 模型。 | 读多写少的场景。允许多个读者同时读,但写者必须独占访问。 |
2.2 锁管理类 (Lock Management)
这是RAII(Resource Acquisition Is Initialization) 理念的体现,用于管理互斥量的锁定和解锁。这是你应该优先使用的方式,它能保证在任何情况下(包括异常发生),锁都能被正确释放,避免死锁。
| 类型 | 说明 | 使用场景 |
|---|---|---|
std::lock_guard (C++11) | 作用域锁。构造时加锁,析构时解锁。非常轻量,效率高。 | 最简单的临界区。不需要额外操作(如手动解锁、条件变量)时,首选。 |
std::unique_lock (C++11) | 功能更丰富的锁管理。比lock_guard功能多,但稍重一点。支持: 1. 延迟加锁 2. 手动解锁 3. 条件变量(必须配合unique_lock) 4. 所有权转移 | 需要更灵活控制锁的场景,例如: 1. 与std::condition_variable配合使用。 2. 不需要立刻锁定的情况。 3. 需要提前解锁以减小锁的粒度。 |
std::shared_lock (C++14) | 共享锁管理。用于std::shared_mutex的“读锁”(共享锁)。 | 配合std::shared_mutex,在只读临界区使用。 |
std::scoped_lock (C++17) | 多互斥量作用域锁。增强版的lock_guard,可以同时锁住多个互斥量而不死锁。 | 需要同时获取多个锁的场景。它使用死锁避免算法,比按顺序调用lock()更安全。 |
3. 详细用法与代码示例
示例 1: 最基本的 std::mutex 和 std::lock_guard
这是最常用、最简单的组合。
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex g_mutex;
int shared_data = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
// 进入作用域时构造lock_guard,自动加锁
std::lock_guard<std::mutex> lock(g_mutex);
// 从这里开始是临界区
++shared_data;
// lock_guard析构,自动解锁
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final value: " << shared_data << std::endl; // 总是 200000
return 0;
}
示例 2: 灵活的 std::unique_lock 和条件变量
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
// 等待条件满足。wait()会临时解锁,避免生产者无法获取锁
cv.wait(lock, []{ return data_ready; });
std::cout << "Consumed!" << std::endl;
}
void producer() {
{
std::lock_guard<std::mutex> lock(mtx);
data_ready = true;
} // lock_guard在这里析构解锁
cv.notify_one(); // 通知消费者
}
int main() {
std::thread t1(consumer);
std::thread t2(producer);
t1.join();
t2.join();
return 0;
}
示例 3: 读写锁 std::shared_mutex (C++17)
cpp
#include <iostream>
#include <thread>
#include <shared_mutex>
std::shared_mutex rw_mutex;
int resource = 0;
void reader(int id) {
for (int i = 0; i < 5; ++i) {
// 使用 shared_lock 获取读锁(共享锁)
std::shared_lock<std::shared_mutex> lock(rw_mutex);
// 多个读者可以同时进入这个临界区
std::cout << "Reader " << id << ": sees " << resource << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void writer(int id) {
for (int i = 0; i < 5; ++i) {
// 使用 unique_lock 或 lock_guard 获取写锁(独占锁)
std::unique_lock<std::shared_mutex> lock(rw_mutex);
// 一次只有一个写者可以进入这个临界区
++resource;
std::cout << "Writer " << id << ": wrote " << resource << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
std::thread readers[3];
std::thread writers[2];
for (int i = 0; i < 2; ++i) writers[i] = std::thread(writer, i);
for (int i = 0; i < 3; ++i) readers[i] = std::thread(reader, i);
for (auto& w : writers) w.join();
for (auto& r : readers) r.join();
return 0;
}
示例 4: 同时锁多个互斥量 std::scoped_lock (C++17)
避免多个锁因顺序问题导致的死锁。
cpp
#include <mutex>
std::mutex mutex1, mutex2;
int a, b; // 需要同时保护的两个数据
void safe_swap(int& x, int& y) {
// 使用scoped_lock一次性锁住两个互斥量,内部使用死锁避免算法
std::scoped_lock lock(mutex1, mutex2);
// 等效于老式的:
// std::lock(mutex1, mutex2); // 同时锁住,避免死锁
// std::lock_guard<std::mutex> lk1(mutex1, std::adopt_lock);
// std::lock_guard<std::mutex> lk2(mutex2, std::adopt_lock);
std::swap(a, b);
}
void unsafe_swap(int& x, int& y) {
// 危险!如果另一个线程以相反顺序锁,会导致死锁!
std::lock_guard<std::mutex> lk1(mutex1);
std::lock_guard<std::mutex> lk2(mutex2);
std::swap(a, b);
}
4. 选择锁的决策流程
面对一个需要同步的场景,你可以遵循以下决策树来选择最合适的工具:

5. 重要注意事项和最佳实践
- 避免死锁(Deadlock):
- 保证互斥量的上锁顺序一致。(
std::scoped_lock可以帮你解决这个问题) - 尽量不要在持有锁的情况下调用未知的外部函数(它可能也会锁,导致顺序混乱)。
- 如果需要获取多个锁,使用
std::lock或std::scoped_lock一次性锁住。
- 保证互斥量的上锁顺序一致。(
- 缩小临界区:锁的粒度要细。只锁住真正共享的数据和操作,锁住后尽快释放,不要在做I/O操作、耗时计算时还持有锁。
- 优先使用RAII管理类:总是优先使用
std::lock_guard,std::unique_lock等,而不是手动调用lock()和unlock()。 - 不要返回共享数据的指针或引用:锁保护的是数据,而不是指针。如果你将受保护数据的引用/指针返回给外部,锁就形同虚设了。
- 性能考量:加锁和解锁是有开销的。对于极简单的操作(如
++counter),可以考虑使用std::atomic无锁编程,性能更高。
6. 练习题
练习 1: std::mutex 和 std::lock_guard
问题:创建一个程序,启动3个线程。每个线程都需要向标准输出(std::cout)顺序地打印10条消息(格式:[Thread ID: <id>] Message: <i>)。如果不加同步,这些消息会交错在一起。请使用 std::mutex 和 std::lock_guard 来同步输出,确保每个线程的消息都是连续且不被打断的。
目标:掌握最基本的互斥量和使用RAII管理锁。
练习 2: std::recursive_mutex
问题:实现一个简单的线程安全的计数器类 RecursiveCounter。它有一个 int 成员 count_ 和两个方法:
void increment(): 将计数器加1。void increment_twice(): 调用两次increment()方法。
在main函数中,创建多个线程来调用increment_twice。如果使用std::mutex,increment_twice中的第二次increment()调用会导致死锁。请使用std::recursive_mutex来避免这个问题。
目标:理解递归互斥量的应用场景——可重入代码。
练习 3: std::timed_mutex
问题:模拟一个需要获取控制权的任务。一个线程(“Controller”)持有 std::timed_mutex 并模拟长时间工作(例如睡眠5秒)。另一个线程(“Client”)尝试使用 try_lock_for() 获取锁,等待最多1秒。如果1秒内获取成功,打印“Successfully took control”;否则,打印“Failed to take control, doing something else…”,然后执行其他任务。
目标:掌握带超时功能的互斥量,用于避免长时间阻塞。
练习 4: std::shared_mutex (读写锁)
问题:模拟一个配置缓存。创建一个 ConfigCache 类,内部用一个 std::map<std::string, int> 存储配置项。该类应提供三个线程安全的方法:
int get(const std::string& key): 读取配置(允许多个读者同时读)。void set(const std::string& key, int value): 设置/更新配置(写者必须独占)。void print_all(): 打印所有配置(读取操作)。
创建多个读者线程和一个写者线程来演示读写锁的行为。读者线程应频繁调用get和print_all,写者线程间歇性地调用set。
目标:理解读写锁的优势(读多写少场景下的性能提升)。
练习 5: std::unique_lock 和 std::condition_variable
问题:实现一个简单的生产者-消费者模型。创建一个固定大小的队列(例如,大小为5)。
- 生产者线程:生产数据(例如,递增的数字),如果队列已满,则等待。
- 消费者线程:消费数据,如果队列为空,则等待。
使用std::mutex、std::unique_lock和std::condition_variable来同步生产者和消费者。std::unique_lock的灵活性(如手动解锁)对于条件变量是必需的。
目标:掌握 unique_lock 与条件变量配合实现复杂的线程间同步。
练习 6: std::scoped_lock
问题:模拟一个需要同时更新两个独立账户的银行转账操作。有两个 Account 对象,各有一个 std::mutex 和一个 balance 成员。编写一个线程安全的 transfer(Account& from, Account& to, int amount) 函数。为了正确转账,必须同时锁住两个账户的互斥量,否则可能发生死锁(例如,线程A锁了账户1准备锁账户2,同时线程B锁了账户2准备锁账户1)。请使用 std::scoped_lock 来安全地同时获取两个锁。
目标:掌握如何安全地同时获取多个互斥量,避免死锁。
7. 解决方案源码
解决方案 1: std::mutex 和 std::lock_guard
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex g_print_mutex;
void print_messages(int thread_id) {
for (int i = 0; i < 10; ++i) {
std::lock_guard<std::mutex> lock(g_print_mutex); // 进入临界区加锁
std::cout << "[Thread ID: " << thread_id << "] Message: " << i << std::endl;
} // 退出作用域,lock_guard析构,自动解锁
}
int main() {
std::thread t1(print_messages, 1);
std::thread t2(print_messages, 2);
std::thread t3(print_messages, 3);
t1.join();
t2.join();
t3.join();
return 0;
}
解决方案 2: std::recursive_mutex
cpp
#include <iostream>
#include <thread>
#include <mutex>
class RecursiveCounter {
private:
int count_ = 0;
std::recursive_mutex mutex_;
public:
void increment() {
std::lock_guard<std::recursive_mutex> lock(mutex_);
++count_;
}
void increment_twice() {
std::lock_guard<std::recursive_mutex> lock(mutex_);
// 因为 mutex_ 是递归的,所以即使在已经上锁的情况下再次调用 increment() 也没问题。
increment(); // 调用一次
increment(); // 再调用一次
}
int get_count() const {
// 注意:为了线程安全地读取,这里也需要加锁。
// 但 const 函数不能修改成员,所以需要使用 mutable 声明 mutex_
// 本例为简化,直接返回,实际生产环境需注意。
return count_;
}
};
int main() {
RecursiveCounter counter;
std::thread t1([&counter]() { counter.increment_twice(); });
std::thread t2([&counter]() { counter.increment_twice(); });
t1.join();
t2.join();
std::cout << "Final count: " << counter.get_count() << std::endl; // 应该是 4
return 0;
}
// 注意:需要在类定义中将 mutable std::recursive_mutex mutex_; 才能让 get_count const 函数加锁
解决方案 3: std::timed_mutex
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::timed_mutex g_timed_mutex;
void controller_work() {
std::cout << "Controller started, locking the mutex for 5s..." << std::endl;
std::lock_guard<std::timed_mutex> lock(g_timed_mutex);
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "Controller finished, releasing the mutex." << std::endl;
}
void client_work() {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 确保Controller先拿到锁
std::cout << "Client trying to get the lock for 1s..." << std::endl;
if (g_timed_mutex.try_lock_for(std::chrono::seconds(1))) {
std::cout << "Successfully took control!" << std::endl;
g_timed_mutex.unlock();
} else {
std::cout << "Failed to take control, doing something else..." << std::endl;
}
}
int main() {
std::thread controller(controller_work);
std::thread client(client_work);
controller.join();
client.join();
return 0;
}
解决方案 4: std::shared_mutex (读写锁)
cpp
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <map>
#include <chrono>
class ConfigCache {
private:
std::map<std::string, int> data_;
mutable std::shared_mutex mutex_; // mutable 允许 const 成员函数上读锁
public:
int get(const std::string& key) const {
std::shared_lock<std::shared_mutex> lock(mutex_); // 读锁(共享)
auto it = data_.find(key);
if (it != data_.end()) {
// 模拟读取耗时
std::this_thread::sleep_for(std::chrono::milliseconds(10));
return it->second;
}
return -1; // Not found
}
void set(const std::string& key, int value) {
std::unique_lock<std::shared_mutex> lock(mutex_); // 写锁(独占)
// 模拟写入耗时
std::this_thread::sleep_for(std::chrono::milliseconds(50));
data_[key] = value;
std::cout << "Set " << key << " = " << value << std::endl;
}
void print_all() const {
std::shared_lock<std::shared_mutex> lock(mutex_); // 读锁(共享)
std::cout << "ConfigCache contents: ";
for (const auto& pair : data_) {
std::cout << pair.first << ":" << pair.second << " ";
}
std::cout << std::endl;
}
};
int main() {
ConfigCache cache;
// 写者线程
std::thread writer([&cache]() {
for (int i = 0; i < 3; ++i) {
cache.set("key" + std::to_string(i), i * 100);
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
});
// 读者线程
std::thread reader1([&cache]() {
for (int i = 0; i < 10; ++i) {
int val = cache.get("key1");
std::cout << "Reader1 got key1: " << val << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
});
std::thread reader2([&cache]() {
for (int i = 0; i < 5; ++i) {
cache.print_all();
std::this_thread::sleep_for(std::chrono::milliseconds(250));
}
});
writer.join();
reader1.join();
reader2.join();
return 0;
}
解决方案 5: std::unique_lock 和 std::condition_variable
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
const int MAX_QUEUE_SIZE = 5;
std::queue<int> g_queue;
std::mutex g_queue_mutex;
std::condition_variable g_cv_producer; // 生产者条件变量:队列不满
std::condition_variable g_cv_consumer; // 消费者条件变量:队列不空
void producer(int id) {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(g_queue_mutex);
// 如果队列满,等待(释放锁,被通知后重新获取锁)
g_cv_producer.wait(lock, [] { return g_queue.size() < MAX_QUEUE_SIZE; });
// 生产数据
int data = id * 100 + i;
g_queue.push(data);
std::cout << "Produced: " << data << " (Queue size: " << g_queue.size() << ")" << std::endl;
lock.unlock(); // 提前解锁,减小临界区
g_cv_consumer.notify_one(); // 通知一个消费者
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
void consumer(int id) {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(g_queue_mutex);
// 如果队列空,等待
g_cv_consumer.wait(lock, [] { return !g_queue.empty(); });
// 消费数据
int data = g_queue.front();
g_queue.pop();
std::cout << "Consumer " << id << " consumed: " << data << std::endl;
lock.unlock();
g_cv_producer.notify_one(); // 通知一个生产者
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
std::thread p1(producer, 1);
std::thread c1(consumer, 1);
std::thread c2(consumer, 2);
p1.join();
c1.join();
c2.join();
return 0;
}
解决方案 6: std::scoped_lock
cpp
#include <iostream>
#include <thread>
#include <mutex>
struct Account {
std::mutex mutex;
int balance;
Account(int initial_balance) : balance(initial_balance) {}
};
void transfer(Account& from, Account& to, int amount) {
if (&from == &to) return; // 自我转账检查
// 关键:使用 std::scoped_lock 一次性锁住两个互斥量
// 它内部使用死锁避免算法,无论顺序如何都不会死锁
std::scoped_lock lock(from.mutex, to.mutex);
// 检查发送方余额是否足够
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
std::cout << "Transferred " << amount << " from Account" << &from << " to Account" << &to << std::endl;
} else {
std::cout << "Insufficient funds in Account" << &from << " for transfer of " << amount << std::endl;
}
} // scoped_lock 在这里析构,自动释放两个锁
int main() {
Account acc1(1000);
Account acc2(500);
std::cout << "Initial balances: " << std::endl;
std::cout << "Account1: " << acc1.balance << std::endl;
std::cout << "Account2: " << acc2.balance << std::endl;
// 启动多个线程进行转账,可能涉及不同的账户顺序
std::thread t1([&]() { transfer(acc1, acc2, 200); }); // A -> B
std::thread t2([&]() { transfer(acc2, acc1, 50); }); // B -> A
t1.join();
t2.join();
std::cout << "Final balances: " << std::endl;
std::cout << "Account1: " << acc1.balance << std::endl;
std::cout << "Account2: " << acc2.balance << std::endl;
return 0;
}
更多推荐
所有评论(0)