一、锁基础与核心概念
锁的作用与类型
- 作用:保护共享资源,防止多线程并发访问导致数据竞争和不一致。
- 类型:
- 互斥锁(Mutex):同一时间仅一个线程可访问资源(如
std::mutex
、QMutex
)。 - 读写锁(ReadWriteLock):允许多个读线程或一个写线程(如
QReadWriteLock
)。 - 自旋锁(SpinLock):忙等待锁,适用于短临界区(无标准库实现,需手动或第三方库)。
- 信号量(Semaphore):控制多资源并发访问(如
QSemaphore
)。
- 互斥锁(Mutex):同一时间仅一个线程可访问资源(如
死锁的产生与避免
- 产生条件:循环等待、互斥、不可抢占、持有并等待。
- 避免策略:
- 固定加锁顺序:所有线程按相同顺序获取锁(例:先锁A再锁B)。
- 超时机制:使用
try_lock_for()
避免永久阻塞。 - 减小锁粒度:仅保护必要数据,缩短持有时间。
- 锁分解:将大锁拆分为多个小锁。
二、C++锁机制(STL)
1. std::lock_guard
vs std::unique_lock
特性 | std::lock_guard |
std::unique_lock |
---|---|---|
灵活性 | 构造时加锁,析构解锁 | 支持手动加/解锁和延迟加锁 |
所有权 | 不可转移 | 可转移(如 std::move ) |
性能 | 更高(无额外开销) | 稍低(功能更多) |
适用场景 | 简单临界区 | 需灵活控制的复杂场景 |
代码示例:
// lock_guard 示例
std::mutex mtx;
{
std::lock_guard<std::mutex> lock(mtx); // 自动加锁
// 临界区操作
} // 自动解锁
// unique_lock 延迟加锁
std::unique_lock<std::mutex> ulock(mtx, std::defer_lock);
ulock.lock(); // 手动加锁
ulock.unlock(); // 手动解锁
2. adopt_lock
/defer_lock
/try_to_lock
用法
std::adopt_lock
:假定锁已被当前线程锁定,构造时不加锁。mtx.lock(); std::lock_guard<std::mutex> lock(mtx, std::adopt_lock); // 直接接管已加锁的mtx
std::defer_lock
:构造时不加锁,后续手动控制。std::try_to_lock
:尝试非阻塞加锁,失败不阻塞。
三、Qt锁机制与多线程同步
1. Qt多线程同步工具
工具 | 用途 | 示例 |
---|---|---|
QMutex |
基础互斥锁 | QMutexLocker locker(&mutex); |
QReadWriteLock |
读写分离锁(多读单写) | QReadLocker rlock(&rwLock); |
QSemaphore |
控制多资源访问(如生产者-消费者) | sem.acquire(); sem.release(); |
生产者-消费者示例:
QSemaphore freeSpace(10); // 缓冲区大小=10
QSemaphore usedSpace(0);
void Producer::run() {
freeSpace.acquire(); // 申请空闲位
// 生产数据
usedSpace.release(); // 增加已使用位
}
void Consumer::run() {
usedSpace.acquire(); // 消费数据
freeSpace.release();
}
2. 信号槽的线程安全控制
- 连接类型:
Qt::DirectConnection
:槽函数在发送者线程执行(跨线程危险)。Qt::QueuedConnection
:槽函数在接收者线程执行(跨线程安全)。
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::QueuedConnection);
四、死锁场景与解决方案
面试题:手写一个线程安全的单例模式(双检锁)
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) { // 第二次检查(避免重复创建)
instance = new Singleton();
}
}
return instance;
}
private:
static Singleton* instance;
static std::mutex mtx;
};
// 初始化静态成员
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
关键点:双检锁减少锁竞争,保证线程安全。
五、线程安全队列实现(C++11版)
#include <queue>
#include <mutex>
#include <condition_variable>
template<typename T>
class SafeQueue {
std::queue<T> queue;
std::mutex mtx;
std::condition_variable cv;
public:
void push(T item) {
std::lock_guard<std::mutex> lock(mtx);
queue.push(item);
cv.notify_one(); // 唤醒一个等待线程
}
T pop() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]{ return !queue.empty(); }); // 避免虚假唤醒
T val = queue.front();
queue.pop();
return val;
}
};
核心机制:
- 条件变量:空队列时阻塞
pop()
线程,直到push()
唤醒。 unique_lock
:配合cv.wait()
需手动解锁需求。
总结与面试技巧
- 基础必考:
- 手写双检锁单例、线程安全队列。
- 解释
lock_guard
与unique_lock
区别。
- 高阶考点:
- Qt信号槽线程控制(
QueuedConnection
)。 - 死锁规避策略(顺序加锁、超时机制)。
- Qt信号槽线程控制(
- 答题技巧:
- 强调边界处理:如空队列
pop()
、锁未释放场景。 - 结合Qt特性:如 “在Qt中优先用
QMutexLocker
而非手动lock/unlock
”。 - 性能对比:读写锁 vs 互斥锁在高读场景下的优势。
- 强调边界处理:如空队列
所有代码需注意异常安全(如锁的RAII管理)和跨平台兼容(Qt锁 vs STL锁)。