面试题及解答:锁

发布于:2025-08-01 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、锁基础与核心概念

  1. 锁的作用与类型

    • 作用:保护共享资源,防止多线程并发访问导致数据竞争和不一致。
    • 类型
      • 互斥锁(Mutex):同一时间仅一个线程可访问资源(如 std::mutexQMutex)。
      • 读写锁(ReadWriteLock):允许多个读线程或一个写线程(如 QReadWriteLock)。
      • 自旋锁(SpinLock):忙等待锁,适用于短临界区(无标准库实现,需手动或第三方库)。
      • 信号量(Semaphore):控制多资源并发访问(如 QSemaphore)。
  2. 死锁的产生与避免

    • 产生条件:循环等待、互斥、不可抢占、持有并等待。
    • 避免策略
      • 固定加锁顺序:所有线程按相同顺序获取锁(例:先锁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() 需手动解锁需求。

总结与面试技巧

  1. 基础必考
    • 手写双检锁单例、线程安全队列。
    • 解释 lock_guardunique_lock 区别。
  2. 高阶考点
    • Qt信号槽线程控制(QueuedConnection)。
    • 死锁规避策略(顺序加锁、超时机制)。
  3. 答题技巧
    • 强调边界处理:如空队列 pop()、锁未释放场景。
    • 结合Qt特性:如 “在Qt中优先用 QMutexLocker 而非手动 lock/unlock”。
    • 性能对比:读写锁 vs 互斥锁在高读场景下的优势。

所有代码需注意异常安全(如锁的RAII管理)和跨平台兼容(Qt锁 vs STL锁)。