You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在类中正确使用std::mutex、std::lock_guard与std::unique_lock?

在类中使用std::mutex、std::lock_guard和std::unique_lock的实用指南

Great question—let's break down how to use these synchronization primitives effectively in a class, since getting this right is make-or-break for thread-safe code.

基础:类中std::mutex的正确定位

首先,类里的std::mutex几乎必须声明为私有成员变量。这样能确保只有类的成员函数能操作它,避免外部代码随意摆弄锁导致死锁或未受保护的资源访问。你给出的示例就是标准的正确用法:

class Foo {
private:
    std::mutex myMutex; // 私有锁,仅类内部可控
public:
    void someFunc() {
        std::lock_guard<std::mutex> guard(myMutex);
        // 线程安全的业务代码
    }
};

什么时候用std::lock_guard(你的常用场景)

std::lock_guard是极简RAII锁管理器,它的核心逻辑就是:

  • 构造时自动锁定传入的mutex
  • 析构时自动解锁(哪怕函数抛出异常也会触发)
  • 没有额外API,不能手动解锁/重新锁定

这让它成为简单场景的首选——比如你只需要在整个函数执行期间保持锁,不需要中途解锁的情况。像成员函数里保护一块连续的临界区,用lock_guard最省心,代码也最简洁。

举个更具体的例子,保护类的私有计数数据:

class Counter {
private:
    std::mutex countMutex;
    int count = 0;
public:
    void increment() {
        std::lock_guard<std::mutex> guard(countMutex);
        count++; // 临界区,线程安全
    }

    int getCount() {
        std::lock_guard<std::mutex> guard(countMutex);
        return count; // 读取也要加锁,避免数据竞争
    }
};

什么时候升级到std::unique_lock

std::unique_lock是lock_guard的超集,它提供了更多灵活性,但代价是稍微高一点的运行开销。你需要用它的场景主要有这几个:

1. 需要中途解锁/重新锁定

如果你的函数里有一段临界区,之后有一段不需要锁的耗时操作(比如IO、复杂计算),这时候可以手动解锁释放锁,让其他线程能访问资源:

class DataProcessor {
private:
    std::mutex dataMutex;
    std::vector<int> rawData;
public:
    void processData() {
        std::unique_lock<std::mutex> lock(dataMutex);
        // 第一步:读取需要保护的数据到本地副本
        std::vector<int> localCopy = rawData;
        lock.unlock(); // 手动解锁,后续计算不需要锁

        // 第二步:耗时的计算,其他线程可以同时修改rawData
        std::transform(localCopy.begin(), localCopy.end(), localCopy.begin(),
                       [](int x) { return x * 2; });

        // 第三步:写回结果,重新加锁
        lock.lock();
        rawData = std::move(localCopy);
    }
};

2. 配合条件变量std::condition_variable

条件变量必须和std::unique_lock一起使用,因为wait()方法会自动解锁mutex并挂起线程,被唤醒时又会重新锁定:

class MessageQueue {
private:
    std::mutex queueMutex;
    std::condition_variable cv;
    std::queue<std::string> messages;
public:
    void pushMessage(const std::string& msg) {
        std::lock_guard<std::mutex> guard(queueMutex);
        messages.push(msg);
        cv.notify_one(); // 通知等待的线程
    }

    std::string popMessage() {
        std::unique_lock<std::mutex> lock(queueMutex);
        // 等待直到队列非空,避免虚假唤醒
        cv.wait(lock, [this]() { return !messages.empty(); });

        std::string msg = messages.front();
        messages.pop();
        return msg;
    }
};

3. 延迟锁定或尝试锁定

std::unique_lock支持构造时不立即锁定,之后手动调用lock(),或者用try_lock()尝试锁定(不会阻塞线程):

class SafeResource {
private:
    std::mutex resMutex;
    bool isInUse = false;
public:
    bool tryAccess() {
        std::unique_lock<std::mutex> lock(resMutex, std::defer_lock); // 延迟锁定
        if (lock.try_lock()) { // 尝试锁定,成功则占用资源
            isInUse = true;
            return true;
        }
        return false; // 锁定失败,资源被占用
    }
};

关键注意事项

  • 锁的粒度要合适:不要用一个大锁保护所有成员函数,尽量把锁的范围缩小到最小的临界区,提高并发性能。比如上面的DataProcessor例子,只在读写rawData时加锁,计算时释放。
  • 避免嵌套锁:如果一个成员函数已经持有锁,调用另一个同样需要锁的成员函数,会导致死锁。如果必须嵌套,考虑用std::recursive_mutex,但尽量重构代码避免这种设计。
  • 异常安全:RAII的好处就是即使临界区抛出异常,锁也会自动释放,但要确保临界区的操作本身是异常安全的(比如避免在锁内做可能导致资源泄漏的操作)。

内容的提问来源于stack exchange,提问作者Francis Cugler

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 08:24:41