在多线程编程中,当多个线程同时访问共享资源时,可能会导致数据竞争(Data Race),产生不可预期的结果。锁提供了同步机制,确保在同一时间只有一个线程可以访问临界区。
锁的本质是通过互斥机制(Mutual Exclusion)确保:
- 同一时间只有一个线程能进入访问共享资源的代码段(临界区);
- 线程对共享资源的修改能被其他线程立即可见(避免 CPU 缓存导致的数据不一致)。
1、标准库锁类型
C++11 及后续标准在<mutex>
头文件中提供了多种锁类型,满足不同场景需求。
1.1 std::mutex
:基础互斥锁
std::mutex
是最基础的互斥锁,提供独占所有权 —— 同一时间仅允许一个线程锁定,其他线程尝试锁定时会阻塞等待,直到锁被释放。
核心操作:
lock()
:锁定互斥锁(若已被锁定,当前线程阻塞);unlock()
:解锁互斥锁(必须由持有锁的线程调用,否则行为未定义);try_lock()
:尝试锁定(成功返回true
,失败返回false
,不阻塞)。
基础用法(手动加锁 / 解锁)
#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx; // 全局互斥锁
int shared_value = 0;// 线程函数:对共享变量累加
void increment() {for (int i = 0; i < 10000; ++i) {mtx.lock(); // 手动加锁shared_value++; // 临界区:安全修改共享资源mtx.unlock(); // 手动解锁(必须执行,否则死锁)}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Final value: " << shared_value << std::endl; // 预期20000return 0;
}
手动调用lock()
和unlock()
存在风险 —— 若临界区抛出异常,unlock()
可能无法执行,导致锁永远被持有(死锁)。因此,实际开发中严禁手动管理锁的生命周期,应使用 RAII 封装。
1.2 std::lock_guard
:RAII 自动管理锁(推荐)
std::lock_guard
是std::mutex
的 RAII(资源获取即初始化)封装类。其构造函数自动调用lock()
,析构函数自动调用unlock()
,确保锁在作用域结束时必然释放(即使发生异常)。
特点:
- 不可复制、不可移动(避免锁所有权被意外转移);
- 生命周期与作用域严格绑定,简单高效。
用lock_guard
避免手动解锁
void safe_increment() {for (int i = 0; i < 10000; ++i) {// 构造时自动加锁,析构时(离开for循环作用域)自动解锁std::lock_guard<std::mutex> lock(mtx); shared_value++; // 临界区安全执行}
}// 主函数同上,最终输出20000(无死锁风险)
优势:即使shared_value++
抛出异常(实际不会),lock_guard
的析构函数仍会被调用,确保锁释放。
1.3 std::unique_lock
:灵活的 RAII 锁
std::unique_lock
比lock_guard
更灵活,支持延迟锁定、手动解锁、所有权转移等操作。其内部维护一个 “是否持有锁” 的状态,因此有额外的性能开销(约几个字节的内存和状态判断)。
核心功能:
- 延迟锁定:构造时不立即加锁(需配合
std::defer_lock
); - 手动控制:可通过
lock()
、unlock()
手动加解锁; - 所有权转移:可通过
std::move()
转移锁的所有权(适合传递锁); - 尝试锁定:支持
try_lock()
和超时锁定(try_lock_for()
、try_lock_until()
)。
延迟锁定与手动控制
void flexible_operation() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟锁定(不立即加锁)// 非临界区操作(无需锁)int temp = 100; lock.lock(); // 手动加锁(进入临界区)shared_value += temp; lock.unlock(); // 提前解锁(退出临界区,让其他线程尽早访问)// 其他非临界区操作
}
超时锁定(避免永久阻塞)
#include <chrono>bool try_operation_with_timeout() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock);// 尝试锁定,最多等待100毫秒if (lock.try_lock_for(std::chrono::milliseconds(100))) {shared_value++;return true; // 锁定成功并执行操作} else {return false; // 超时未获取锁,执行备选逻辑}
}
1.4 std::recursive_mutex
:递归锁(同一线程可重入)
std::mutex
不允许同一线程重复锁定(会导致死锁),而std::recursive_mutex
允许同一线程多次调用lock()
,但需对应相同次数的unlock()
才能完全释放(内部维护 “递归计数”)。
递归函数中需要重复锁定同一资源(如二叉树遍历中,递归访问节点时需锁定全局计数器)。
递归函数中的锁重入
#include <recursive_mutex>std::recursive_mutex rmtx;
int count = 0;// 递归函数:每次递归都需要锁定
void recursive_count(int depth) {if (depth <= 0) return;std::lock_guard<std::recursive_mutex> lock(rmtx); // 同一线程可多次锁定count++; recursive_count(depth - 1); // 递归调用,再次锁定
}int main() {recursive_count(5);std::cout << "Count: " << count << std::endl; // 输出5(正确累加)return 0;
}
注意:递归锁易掩盖设计缺陷(如过度依赖共享资源),非必要不使用(优先考虑拆分临界区)
1.5 std::shared_mutex
(C++17):读写分离锁
std::shared_mutex
(或 C++14 的std::shared_timed_mutex
)支持两种锁定模式,适用于 “读多写少” 场景:
- 共享锁(读锁):多个线程可同时获取,用于读取共享资源(不修改);
- 独占锁(写锁):仅一个线程可获取,用于修改共享资源(此时所有读锁和写锁均阻塞)。
通过分离读写操作,提高读操作的并发性能(读线程间无需互斥)。
读写分离控制
#include <shared_mutex>
#include <vector>std::shared_mutex smtx;
std::vector<int> data = {1, 2, 3}; // 共享数据// 读操作:获取共享锁(允许多线程同时读)
int read_data(int index) {std::shared_lock<std::shared_mutex> lock(smtx); // 共享锁return data[index];
}// 写操作:获取独占锁(仅允许单线程写)
void write_data(int index, int value) {std::unique_lock<std::shared_mutex> lock(smtx); // 独占锁data[index] = value;
}int main() {// 多个读线程可并发执行read_data()std::thread t1([]{ std::cout << read_data(0) << std::endl; });std::thread t2([]{ std::cout << read_data(1) << std::endl; });// 写线程执行时,读线程需等待std::thread t3([]{ write_data(0, 100); });t1.join(); t2.join(); t3.join();return 0;
}
2、死锁
2.1 死锁的产生条件
死锁是指两个或多个线程相互等待对方释放锁,导致永久阻塞的状态,需同时满足:
- 互斥:锁被线程独占;
- 持有并等待:线程持有一个锁,同时等待另一个锁;
- 不可剥夺:线程持有的锁不能被强制释放;
- 循环等待:线程间形成等待环(如线程 1 等锁 B,线程 2 等锁 A)。
2.2 错误示范
std::mutex mtx_a, mtx_b;// 线程1:先锁A,再等B
void thread1() {std::lock_guard<std::mutex> lock_a(mtx_a);// 模拟临界区操作(给线程2抢锁时间)std::this_thread::sleep_for(std::chrono::milliseconds(10));std::lock_guard<std::mutex> lock_b(mtx_b); // 等待线程2释放B → 死锁
}// 线程2:先锁B,再等A
void thread2() {std::lock_guard<std::mutex> lock_b(mtx_b);std::this_thread::sleep_for(std::chrono::milliseconds(10));std::lock_guard<std::mutex> lock_a(mtx_a); // 等待线程1释放A → 死锁
}int main() {std::thread t1(thread1);std::thread t2(thread2);t1.join(); // 程序永久阻塞(死锁)t2.join();return 0;
}
2.3 避免死锁的核心方法
2.3.1 按固定顺序加锁
所有线程按相同的全局顺序锁定多个锁(如始终先锁mtx_a
,再锁mtx_b
),打破 “循环等待” 条件。
// 线程1和线程2均按"先A后B"的顺序加锁
void thread1_fixed() {std::lock_guard<std::mutex> lock_a(mtx_a);std::lock_guard<std::mutex> lock_b(mtx_b); // 不会死锁
}void thread2_fixed() {std::lock_guard<std::mutex> lock_a(mtx_a); // 先等A,再锁Bstd::lock_guard<std::mutex> lock_b(mtx_b);
}
2.3.2 用std::lock
原子锁定多个锁
std::lock
可原子地同时锁定多个互斥量(内部通过复杂逻辑避免死锁),适合无法固定顺序的场景。
void safe_lock_two() {// 原子锁定mtx_a和mtx_b(无顺序依赖)std::lock(mtx_a, mtx_b); // 用adopt_lock标记锁已被锁定,避免重复加锁std::lock_guard<std::mutex> lock_a(mtx_a, std::adopt_lock);std::lock_guard<std::mutex> lock_b(mtx_b, std::adopt_lock);// 临界区操作
}
2.3.3 减少锁的持有时间
临界区仅包含必要操作,锁尽早释放(如用unique_lock
手动解锁),降低死锁概率。
3、锁的选择与性能考量
锁类型 | 核心优势 | 性能开销 | 适用场景 |
---|---|---|---|
std::mutex +lock_guard |
简单安全,无额外开销 | 低 | 大多数场景,临界区简单且短 |
std::unique_lock |
支持延迟锁定、超时、所有权转移 | 中 | 需要灵活控制锁生命周期的场景 |
std::recursive_mutex |
允许同一线程重入 | 中高 | 递归函数需重复锁同一资源(谨慎使用) |
std::shared_mutex |
读写分离,提高读并发 | 中 | 读多写少,如缓存、配置数据访问 |
C++ 锁通过互斥机制解决多线程共享资源的竞态条件问题,其核心是确保临界区原子性。实际开发中:
- 优先使用 RAII 封装(
lock_guard
/unique_lock
)避免手动管理锁的风险; - 根据场景选择锁类型(如读多写少用
shared_mutex
); - 通过固定加锁顺序、
std::lock
等方法严格避免死锁。