锁是一种同步原语,用于保证多个线程在访问共享资源时的互斥性。通过加锁机制,可以确保在某一时刻,只有一个线程能够访问共享资源。
锁类型 | 特点 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
互斥锁 (std::mutex) | 简单的二进制锁,线程间互斥访问共享资源 | 实现简单、适用广泛 | 阻塞线程,可能导致上下文切换开销 | 共享资源需要严格互斥的场景 |
递归锁 (std::recursive_mutex) | 同一线程可以多次加锁,无需担心死锁 | 避免递归调用时死锁问题 | 性能略差于普通互斥锁 | 递归函数需要加锁的场景 |
读写锁 (std::shared_mutex) | 多线程可并发读取,但写操作独占 | 提高读操作多的场景下的并发性能 | 写操作需要独占锁,读多写少时性能最佳 | 数据读多写少的场景 |
自旋锁 (std::atomic_flag) | 线程忙等待,不阻塞,适合短期锁 | 低延迟,无需上下文切换 | 忙等待消耗CPU资源,不适合长时间锁持有 | 短期锁定操作或实时性高的场景 |
互斥锁(Mutex)是最基础的锁,通过阻塞线程保证互斥性。C++ 的 std::mutex
提供基础实现。
互斥锁用于保护共享资源的同步机制。当一个线程想要访问一个被互斥锁保护的资源时,它必须首先获取锁。如果锁已经被其他线程持有,那么这个线程就会被阻塞,直到锁被释放。以下是互斥锁的工作流程:
在这个流程图中:
这就是互斥锁的基本工作流程。通过这种方式,互斥锁可以确保任何时候都只有一个线程能够访问被保护的资源,从而避免了数据的不一致性。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int counter = 0;
void safeIncrement() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁和解锁
++counter;
}
}
int main() {
std::thread t1(safeIncrement);
std::thread t2(safeIncrement);
t1.join();
t2.join();
std::cout << "Final counter: " << counter << std::endl;
return 0;
}
递归锁(Recursive Mutex)允许同一线程多次加锁,而不会导致死锁。通常用于递归函数中。
递归锁是一种特殊类型的互斥锁,它允许同一个线程多次获取同一个锁,而不会造成死锁。这在某些需要多次访问同一资源的场景中非常有用,例如递归函数。以下是递归锁的工作流程:
在这个流程图中:
这就是递归锁的基本工作流程。通过这种方式,递归锁可以避免同一个线程因多次获取同一个锁而造成的死锁,从而使得代码更加简洁和易于理解。
#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex rec_mtx;
void recursiveFunction(int depth) {
if (depth <= 0) return;
rec_mtx.lock();
std::cout << "Lock acquired at depth: " << depth << std::endl;
recursiveFunction(depth - 1);
rec_mtx.unlock();
}
int main() {
std::thread t1(recursiveFunction, 5);
t1.join();
return 0;
}
读写锁(Read-Write Lock)允许多线程并发读取,但写操作需要独占。C++17 提供了 std::shared_mutex
。
读写锁是一种特殊类型的锁,它允许多个读线程同时访问资源,但在写线程访问资源时,所有其他线程(无论是读线程还是写线程)都不能访问资源。以下是读写锁的工作流程:
在这个流程图中:
这就是读写锁的基本工作流程。通过这种方式,读写锁可以在保证数据一致性的同时,提高读操作的并发性能。
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
std::shared_mutex rw_lock;
std::vector<int> data;
void reader() {
std::shared_lock<std::shared_mutex> lock(rw_lock); // 读锁
for (const auto& val : data) {
std::cout << "Reader sees: " << val << std::endl;
}
}
void writer(int value) {
std::unique_lock<std::shared_mutex> lock(rw_lock); // 写锁
data.push_back(value);
std::cout << "Writer adds: " << value << std::endl;
}
int main() {
std::thread t1(reader);
std::thread t2(writer, 42);
std::thread t3(reader);
t1.join();
t2.join();
t3.join();
return 0;
}
自旋锁(Spinlock)是一种特殊类型的锁,当线程无法立即获取锁时,它不会立即进入阻塞状态,而是在一个循环中不断地尝试获取锁,直到成功为止。这种锁适用于锁持有时间短且线程不希望在重新调度上花费过多时间的情况。
以下是自旋锁的工作流程:
在这个流程图中:
这就是自旋锁的基本工作流程。通过这种方式,自旋锁可以避免线程在等待锁时进入阻塞状态,从而减少了线程调度的开销。
#include <atomic>
#include <thread>
#include <iostream>
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void spinlockTask(int id) {
while (lock.test_and_set(std::memory_order_acquire)) {
// 自旋等待,直到获取到锁
}
std::cout << "Thread " << id << " acquired the lock." << std::endl;
lock.clear(std::memory_order_release);
}
int main() {
std::thread t1(spinlockTask, 1);
std::thread t2(spinlockTask, 2);
t1.join();
t2.join();
return 0;
}
在这个代码示例中,我们使用了C++的std::atomic_flag
来实现一个简单的自旋锁。当一个线程无法立即获取锁时,它会在一个循环中不断地尝试获取锁,直到成功为止。
**死锁(Deadlock)**是指两个或多个线程或进程因争夺资源而相互等待,导致所有参与者都无法继续执行的状态。
死锁是并发编程中的一种常见问题,它发生在两个或更多的进程或线程互相等待对方释放资源,导致所有的进程或线程都无法继续执行。以下是死锁的一些常见表现:
1. 系统停滞
最明显的死锁表现就是系统停滞,也就是说,系统的一部分或全部都无法继续执行。这可能表现为用户界面无响应,或者后台服务停止工作。
2. 高CPU使用率
在某些情况下,死锁可能会导致CPU使用率异常升高。这是因为,当进程或线程在等待资源时,它们可能会不断地进行无效的轮询,从而消耗大量的CPU时间。
3. 日志停止更新
如果系统有日志记录,那么在死锁发生时,日志可能会停止更新。这是因为,进程或线程在等待资源时,它们无法执行其他的操作,包括写日志。
4. 资源占用不释放
在死锁发生时,相关的资源可能会被永久占用,而无法被释放。这可能表现为内存占用过高,或者文件、数据库连接等资源无法关闭。
根据 Coffman 在 1971 年提出的理论,死锁的发生需要满足以下四个条件,这些条件同时成立时,系统可能进入死锁状态:
条件 | 描述 |
---|---|
互斥(Mutual Exclusion) | 至少有一个资源是非共享的,某一时刻只能被一个线程或进程占用。 |
请求与保持(Hold and Wait) | 线程已经持有资源,同时又请求新的资源,但未释放已有资源。 |
不可剥夺(No Preemption) | 已被分配的资源不能强制剥夺,只能由持有该资源的线程或进程主动释放。 |
循环等待(Circular Wait) | 存在一个线程或进程的循环等待链,链中的每个线程或进程都在等待下一个线程持有的资源。 |
以下代码展示了一个简单的死锁场景:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void thread1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟操作延迟
std::lock_guard<std::mutex> lock2(mtx2); // 等待 mtx2
}
void thread2() {
std::lock_guard<std::mutex> lock2(mtx2);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟操作延迟
std::lock_guard<std::mutex> lock1(mtx1); // 等待 mtx1
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
mtx1
,等待 mtx2
;线程 2 持有 mtx2
,等待 mtx1
。要避免死锁,可以通过以下策略打破上述四个条件之一:
策略:为资源分配顺序,所有线程按固定顺序请求资源。
实现:在多锁操作时使用 std::lock
,保证同时锁定多个资源。
std::lock(mtx1, mtx2);
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
策略:线程在请求资源前必须释放已持有的资源。 实现:确保资源释放后再进行下一步操作,避免长时间持有锁。
std::unique_lock<std::mutex> lock(mtx);
processData();
lock.unlock(); // 提前释放资源
performOtherTasks();
策略:使用能够超时的锁操作,如 std::unique_lock
的超时版本,避免无限等待。
实现:
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
if (lock.owns_lock()) {
processData();
} else {
std::cout << "Resource is busy, retry later." << std::endl;
}
策略:在某些情况下,可以通过无锁编程实现数据共享,避免资源互斥。
实现:使用 std::atomic
或无锁数据结构替代互斥锁。
在操作系统中,常用**资源分配图(Resource Allocation Graph,RAG)**来检测死锁:
以下代码展示了通过 std::lock
避免死锁的优化:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
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);
std::cout << "Thread 1 acquired both locks." << std::endl;
}
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);
std::cout << "Thread 2 acquired both locks." << std::endl;
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
通过 std::lock
保证加锁顺序一致,从而避免死锁。
std::lock_guard
)、加锁策略(如 std::lock
)以及无锁编程技术避免死锁。在实际开发中,预防死锁比处理死锁更为重要,应优先通过良好的设计和编码实践避免死锁的发生。
加锁确实会带来性能问题,如上下文切换、锁争用等待、死锁等影响程序并发性能的问题。以下是针对加锁导致的性能问题的分析及优化策略:
问题描述:锁粒度过大(例如整个函数加锁)会降低并发性。 解决方法:将大锁拆分为小锁,尽量缩小锁的作用范围,只保护真正需要保护的关键代码段。
示例 优化前:
std::mutex mtx;
void processData() {
std::lock_guard<std::mutex> lock(mtx);
// 临界区代码
criticalSection();
// 非临界区代码
nonCriticalSection();
}
优化后:
std::mutex mtx;
void processData() {
criticalSection(); // 非加锁部分
{
std::lock_guard<std::mutex> lock(mtx);
criticalSection(); // 临界区代码
}
nonCriticalSection(); // 非加锁部分
}
问题描述:标准互斥锁对读写操作一视同仁,而大多数场景中读操作多于写操作。
解决方法:使用读写锁(如 std::shared_mutex
),允许多个线程同时读,写时加独占锁。
示例
#include <shared_mutex>
#include <vector>
std::shared_mutex rwLock;
std::vector<int> data;
void reader() {
std::shared_lock<std::shared_mutex> lock(rwLock); // 读锁
for (const auto& val : data) {
std::cout << "Read: " << val << std::endl;
}
}
void writer(int value) {
std::unique_lock<std::shared_mutex> lock(rwLock); // 写锁
data.push_back(value);
std::cout << "Wrote: " << value << std::endl;
}
适用场景:数据读多写少的场景(如缓存系统、日志记录等)。
问题描述:锁的等待和上下文切换增加了开销。
解决方法:在某些场景下,可以使用无锁数据结构(如 std::atomic
或自定义的无锁队列)替代加锁。
示例 无锁计数器:
#include <atomic>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // 原子操作,无需加锁
}
}
适用场景:简单的计数、标志位等共享数据操作。
问题描述:锁持有时间过长可能导致其他线程阻塞,系统吞吐量降低。 解决方法:减少临界区的执行时间,将耗时操作移出锁范围。
示例 优化前:
std::mutex mtx;
void process() {
std::lock_guard<std::mutex> lock(mtx);
timeConsumingTask(); // 耗时操作
}
优化后:
std::mutex mtx;
void process() {
prepareData(); // 耗时操作,移出锁范围
std::lock_guard<std::mutex> lock(mtx);
updateCriticalSection(); // 临界区代码
}
问题描述:线程可能因等待锁而长时间阻塞。
解决方法:使用 std::try_lock
或 std::unique_lock
的超时功能避免长时间阻塞。
示例
std::mutex mtx;
void process() {
if (mtx.try_lock()) { // 尝试加锁
std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
// 临界区代码
} else {
std::cout << "Lock is busy, retry later." << std::endl;
}
}
问题描述:一个大锁可能导致多个无关线程的操作被序列化。 解决方法:将数据划分为多个分区,每个分区使用独立的锁,从而提高并发性能。
示例
#include <mutex>
#include <vector>
std::vector<std::mutex> locks(10); // 每个分区一个锁
std::vector<int> data(10, 0);
void updatePartition(int index, int value) {
std::lock_guard<std::mutex> lock(locks[index]);
data[index] += value;
}
问题描述:加锁可能并不是唯一的同步方式。 解决方法:使用条件变量、信号量、事件驱动模型等替代锁。
示例:条件变量
#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> dataQueue;
void producer() {
std::unique_lock<std::mutex> lock(mtx);
dataQueue.push(42);
cv.notify_one(); // 通知消费者
}
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !dataQueue.empty(); }); // 等待数据
int value = dataQueue.front();
dataQueue.pop();
std::cout << "Consumed: " << value << std::endl;
}
问题描述:大量的锁往往是因为线程竞争共享资源。 解决方法:采用消息队列、任务分解等方式,避免直接共享数据。
示例:任务队列
#include <queue>
#include <mutex>
#include <thread>
#include <iostream>
std::queue<int> taskQueue;
std::mutex mtx;
void worker() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
if (!taskQueue.empty()) {
int task = taskQueue.front();
taskQueue.pop();
lock.unlock(); // 提前释放锁
std::cout << "Processing task: " << task << std::endl;
} else {
lock.unlock(); // 提前释放锁
std::this_thread::yield(); // 暂时让出CPU
}
}
}
int main() {
std::thread t(worker);
{
std::lock_guard<std::mutex> lock(mtx);
taskQueue.push(42); // 添加任务
}
t.join();
return 0;
}
问题 | 解决方案 |
---|---|
锁粒度过大 | 缩小锁粒度,仅对必要代码段加锁 |
锁争用严重 | 使用读写锁或分区锁分散锁竞争 |
长时间持锁 | 将耗时操作移出临界区,减小锁的持有时间 |
频繁上下文切换 | 使用无锁数据结构或自旋锁减少阻塞开销 |
线程长时间等待锁 | 使用尝试加锁(try_lock)或超时锁机制 |
需要高性能并发 | 使用无锁数据结构、分区锁、或异步编程(如任务队列)优化 |
本文介绍了锁的基本概念、不同类型锁的特点及实现方法,并详细阐述了死锁的原理、示例及预防策略。在实际开发中,选择合适的锁类型和避免死锁是并发编程的核心,以下几点需要牢记:
万字长文码字不易,感谢点赞、收藏、关注支持