如何在类中正确使用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

