14.线程锁
目录介绍
- 14.1 锁的概念
- 14.1.1 为何需要锁
- 14.1.2 C++锁机制发展
- 14.2 互斥锁(Mutex)
- 14.2.1 mutex
- 14.2.2 lock_guard
- 14.2.3 unique_lock
- 12.2.4 recursive_mutex
- 12.2.5 timed_mutex
- 14.3 读写锁(Read-Write Lock)
- 14.3.1 shared_mutex
- 14.4 条件变量(Condition Variable)
- 14.4.1 condition_variable
- 14.4.2 核心方法
- 14.4.3 condition_variable案例
- 14.5 死锁与避免
- 14.5.1 死锁示例
- 14.5.2 避免死锁
- 14.5.3 排查死锁
14.1 锁的概念
在 C++ 中,锁 是用于多线程编程的同步机制,用于保护共享资源,避免数据竞争和并发访问导致的问题。
C++ 标准库提供了多种锁的实现,包括互斥锁、读写锁、条件变量等。
14.1.1 为何需要锁
在多线程环境中,多个线程可能同时访问或修改共享资源(如全局变量、数据结构等)。如果没有同步机制,可能会导致以下问题:
- 数据竞争: 多个线程同时修改同一数据,导致数据不一致或程序行为异常。
- 竞态条件: 程序的执行结果依赖于线程的执行顺序,导致不可预测的行为。
线程锁通过确保同一时间只有一个线程可以访问共享资源来解决这些问题。
14.1.2 C++锁机制发展
- C++11:引入基本并发原语 (
mutex,lock_guard,condition_variable) - C++14:增强时间相关操作 (
try_lock_for) - C++17:添加
scoped_lock和shared_mutex - C++20:引入信号量 (
std::counting_semaphore) 和原子智能指针
14.2 互斥锁(Mutex)
互斥锁是最常用的锁类型,用于确保同一时间只有一个线程可以访问共享资源。
14.2.1 mutex
std::mutex,基本的互斥锁。使用 lock() 和 unlock() 手动管理锁。使用 std::lock_guard 或 std::unique_lock 自动管理锁。
1.作用:
- lock(): 尝试获取互斥量的所有权。如果互斥量已被其他线程锁定,则当前线程会被阻塞,直到互斥量被解锁。
- unlock(): 释放互斥量的所有权,允许其他线程获取该互斥量。
示例:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥量
int sharedData = 0;
void increment() {
for (int i = 0; i < 1000; ++i) {
mtx.lock(); // 加锁
++sharedData;
mtx.unlock(); // 解锁
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Shared Data: " << sharedData << std::endl; // 输出: Shared Data: 2000
return 0;
}
2.原理: 互斥量(mutex)是操作系统提供的一种同步机制,用于保护共享资源,防止多个线程同时访问造成数据竞争。其底层实现通常依赖于操作系统的原子操作和线程调度机制。
- 获取锁(lock):当一个线程调用lock()时:如果锁当前是空闲状态(未被任何线程持有),则当前线程获得锁,并继续执行。如果锁已被其他线程持有,当前线程会被操作系统挂起(进入阻塞状态),并放入该锁的等待队列中。
- 释放锁(unlock):当一个线程调用unlock()时:锁被释放,变为空闲状态。操作系统会唤醒等待该锁的一个线程(或所有线程,具体取决于实现),被唤醒的线程将尝试获取锁(其中一个会成功,其余可能再次阻塞)。
3.为什么需要手动管理锁?在某些复杂场景下,自动锁管理(如std::lock_guard)可能不够灵活。手动管理锁提供了更精细的控制,例如:
- 需要跨越多个作用域的锁。
- 需要根据条件决定何时释放锁。
- 需要配合条件变量使用(std::condition_variable要求使用std::unique_lock,其内部就是手动管理)。
4.手动管理锁注意事项
- 异常安全:手动管理锁需要特别注意异常安全。如果在临界区(lock()和unlock()之间)抛出异常,可能导致锁无法释放,引发死锁。解决方案:使用RAII对象(如std::lock_guard或std::unique_lock)自动管理。
- 死锁风险:手动管理多个互斥量时容易因加锁顺序不一致导致死锁。解决方案:使用std::lock(mtx1, mtx2)函数同时锁定多个互斥量(避免死锁)。
- 性能考虑:频繁的加锁/解锁会增加系统开销。锁的粒度(锁保护的代码范围)要合理:太大会降低并发性,太小会增加锁开销。
14.2.2 lock_guard
自动管理锁的生命周期,离开作用域时自动释放锁。适用于简单的加锁场景。
1.lock_guard的作用
- 自动加锁:在构造时自动锁定互斥锁。
- 自动解锁:在析构时自动释放互斥锁。
- 防止死锁:确保在作用域结束时互斥锁一定会被释放,即使发生异常。
2.lock_guard设计思想
- RAII(Resource Acquisition Is Initialization):将资源的生命周期与对象的生命周期绑定。资源在对象构造时获取,在对象析构时释放。
- 简化资源管理:通过自动管理资源的获取和释放,减少手动管理资源的错误(如忘记解锁或异常导致未解锁)。
3.lock_guard的原理
- 构造函数: 在构造时,
std::lock_guard会调用std::mutex的lock()方法,锁定互斥锁。如果互斥锁已被其他线程锁定,当前线程会阻塞,直到锁可用。 - 析构函数: 在析构时,
std::lock_guard会调用std::mutex的unlock()方法,释放互斥锁。即使发生异常,析构函数也会被调用,确保锁一定会被释放。 - 不可复制:
std::lock_guard是不可复制的,因为复制会导致多个对象管理同一个锁,从而引发未定义行为。
4.使用示例。以下是一个简单的
std::lock_guard使用示例:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx; // 全局互斥锁
int shared_data = 0; // 共享数据
void increment() {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁
++shared_data; // 操作共享数据
// 离开作用域时自动解锁
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Shared data: " << shared_data << std::endl; // 输出 2
return 0;
}
5.局限性
- 作用域限制:
std::lock_guard的生命周期受限于作用域,无法手动控制锁的释放。 - 不可移动:
std::lock_guard是不可移动的,无法转移锁的所有权。
14.2.3 unique_lock
std::unique_lock 是一个功能强大的工具,提供了比 std::lock_guard 更多的灵活性和控制选项。它适用于需要手动控制锁、延迟加锁、锁的所有权转移或与条件变量配合使用的场景。在简单场景中,std::lock_guard 是更轻量级的选择;而在复杂场景中,std::unique_lock 是更合适的选择。
1.操作的api是:
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
if(lock.try_lock()) {
// 操作...
lock.unlock(); // 手动解锁
}
2.
std::unique_lock的主要作用是
- 自动加锁和解锁:在构造时可以选择自动加锁,在析构时自动解锁。
- 手动控制锁:支持手动加锁(
lock())和解锁(unlock())。 - 延迟加锁:支持延迟加锁(
defer_lock),在构造时不立即加锁。 - 锁的所有权转移:支持移动语义,可以将锁的所有权从一个
std::unique_lock对象转移到另一个。 - 条件变量支持:与
std::condition_variable配合使用,支持条件变量的等待操作。
3.
std::unique_lock的设计思想
- RAII(Resource Acquisition Is Initialization): 将资源的生命周期与对象的生命周期绑定,确保资源在对象构造时获取,在对象析构时释放。
- 灵活性: 提供多种加锁策略(如立即加锁、延迟加锁、尝试加锁等),适应不同的使用场景。
- 所有权管理: 通过移动语义支持锁的所有权转移,允许锁的管理权在对象之间传递。
4.
std::unique_lock的实现原理如下:
- 构造函数:支持多种构造函数,可以选择立即加锁、延迟加锁或尝试加锁。例如:
std::unique_lock<std::mutex> lock(mtx); // 立即加锁 std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁 - 析构函数:在析构时,如果锁被持有,则自动调用
unlock()释放锁。确保锁一定会被释放,即使发生异常。 - 手动控制:提供
lock()和unlock()方法,允许手动控制锁的状态。例如:std::unique_lock<std::mutex> lock(mtx, std::defer_lock); lock.lock(); // 手动加锁 lock.unlock(); // 手动解锁 - 移动语义:支持移动构造函数和移动赋值运算符,允许锁的所有权转移。例如:
std::unique_lock<std::mutex> lock1(mtx); std::unique_lock<std::mutex> lock2 = std::move(lock1); // 所有权转移 - 条件变量支持:可以与
std::condition_variable配合使用,支持等待操作。例如:std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return ready; }); // 等待条件变量
示例:
void increment() {
for (int i = 0; i < 1000; ++i) {
std::unique_lock<std::mutex> lock(mtx); // 自动加锁和解锁
++sharedData;
lock.unlock(); // 手动解锁
}
}
std::unique_lock 与 std::lock_guard 的区别
| 特性 | std::lock_guard | std::unique_lock |
|---|---|---|
| 加锁策略 | 立即加锁 | 支持立即加锁、延迟加锁、尝试加锁 |
| 手动控制 | 不支持 | 支持手动加锁和解锁 |
| 所有权转移 | 不支持 | 支持移动语义,允许所有权转移 |
| 条件变量支持 | 不支持 | 支持与 std::condition_variable 配合 |
| 性能 | 更轻量,性能更高 | 更灵活,但性能稍低 |
12.2.4 recursive_mutex
std::recursive_mutex 是 C++ 标准库中的一种互斥锁类型,允许同一线程多次加锁而不会导致死锁。它适用于需要递归加锁的场景,例如在递归函数或嵌套调用中。
1.
std::recursive_mutex的主要作用是:
- 递归加锁:允许同一线程多次加锁,而不会导致死锁。
- 嵌套锁管理:在递归函数或嵌套调用中,确保锁的正确获取和释放。
- 线程安全:确保多线程环境下共享资源的互斥访问。
2.
std::recursive_mutex的设计基于以下思想:
- 递归锁机制: 允许同一线程多次加锁,每次加锁都会增加锁的计数。只有当锁的计数减到 0 时,锁才会被释放。
- 避免死锁: 在递归调用或嵌套加锁的场景中,避免因同一线程重复加锁而导致的死锁。
- 线程安全: 确保多线程环境下,同一时间只有一个线程可以持有锁。
3.
std::recursive_mutex的实现原理如下:
- 锁计数:
- 每个
std::recursive_mutex内部维护一个锁计数和一个线程 ID。 - 当线程第一次加锁时,锁计数加 1,并记录当前线程 ID。
- 当同一线程再次加锁时,锁计数加 1,而不会阻塞。
- 当线程解锁时,锁计数减 1;只有当锁计数减到 0 时,锁才会被释放。
- 每个
- 线程检查: 如果其他线程尝试加锁,而锁已被当前线程持有,则其他线程会阻塞,直到锁被释放。
- 与
std::mutex的区别:std::mutex不允许同一线程多次加锁,否则会导致未定义行为或死锁。std::recursive_mutex允许同一线程多次加锁,适用于递归或嵌套加锁的场景。
以下是一个简单的 std::recursive_mutex 使用示例:
#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex mtx; // 递归互斥锁
void recursive_function(int n) {
std::lock_guard<std::recursive_mutex> lock(mtx); // 加锁
if (n > 0) {
std::cout << "Thread " << std::this_thread::get_id() << ": n = " << n << std::endl;
recursive_function(n - 1); // 递归调用
}
// 离开作用域时自动解锁
}
int main() {
std::thread t1(recursive_function, 3);
std::thread t2(recursive_function, 2);
t1.join();
t2.join();
return 0;
}
输出示例:
Thread 140735680944896: n = 3
Thread 140735680944896: n = 2
Thread 140735680944896: n = 1
Thread 140735672552192: n = 2
Thread 140735672552192: n = 1
12.2.5 timed_mutex
std::timed_mutex 是一种支持超时加锁的互斥锁,提供了 try_lock_for() 和 try_lock_until() 方法。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::timed_mutex tmtx;
void tryLockFunction() {
if (tmtx.try_lock_for(std::chrono::milliseconds(100))) { // 尝试加锁,最多等待 100ms
std::cout << "Lock acquired!" << std::endl;
tmtx.unlock();
} else {
std::cout << "Failed to acquire lock!" << std::endl;
}
}
int main() {
std::thread t1(tryLockFunction);
std::thread t2(tryLockFunction);
t1.join();
t2.join();
return 0;
}
14.3 读写锁(Read-Write Lock)
读写锁允许多个线程同时读取共享资源,但写操作需要独占访问。
14.3.1 shared_mutex
C++17 引入的读写锁。
- 使用
lock_shared()和unlock_shared()进行读锁定。 - 使用
lock()和unlock()进行写锁定。
示例:
#include <iostream>
#include <thread>
#include <shared_mutex>
std::shared_mutex rwMutex;
int sharedData = 0;
void readData() {
std::shared_lock<std::shared_mutex> lock(rwMutex); // 读锁定
std::cout << "Read Data: " << sharedData << std::endl;
}
void writeData() {
std::unique_lock<std::shared_mutex> lock(rwMutex); // 写锁定
++sharedData;
std::cout << "Write Data: " << sharedData << std::endl;
}
int main() {
std::thread t1(readData);
std::thread t2(writeData);
t1.join();
t2.join();
return 0;
}
14.4 条件变量(Condition Variable)
14.4.1 condition_variable
条件变量用于线程间的同步,允许线程等待某个条件成立。
- 与
std::mutex配合使用。 - 使用
wait()等待条件,notify_one()或notify_all()通知等待的线程。
14.4.2 核心方法
核心方法
wait(lock); // 等待条件
wait(lock, predicate); // 带条件谓词的等待
notify_one(); // 通知一个等待线程
notify_all(); // 通知所有等待线程
14.4.3 condition_variable案例
示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void waitForReady() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // 等待条件成立
std::cout << "Ready!" << std::endl;
}
void setReady() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 通知等待的线程
}
int main() {
std::thread t1(waitForReady);
std::thread t2(setReady);
t1.join();
t2.join();
return 0;
}
14.5 死锁与避免
死锁是指多个线程互相等待对方释放锁,导致程序无法继续执行。
14.5.1 死锁示例
std::mutex mtx1, mtx2;
void thread1() {
mtx1.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
mtx2.lock(); // 等待 mtx2
mtx2.unlock();
mtx1.unlock();
}
void thread2() {
mtx2.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
mtx1.lock(); // 等待 mtx1
mtx1.unlock();
mtx2.unlock();
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
14.5.2 避免死锁
- 按固定顺序加锁。
- 使用
std::lock()同时锁定多个互斥锁。
示例:
void thread1() {
std::lock(mtx1, mtx2); // 同时锁定
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 操作共享资源
}
void thread2() {
std::lock(mtx1, mtx2); // 同时锁定
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 操作共享资源
}