结论先行:不必须,而且“放在作用域最后”这个说法本身就是个误解。

真正的规则只有一句话 👇

lock_guard(m, adopt_lock) 必须在「已经成功加锁之后」构造,并且它的作用域要覆盖你希望持锁的代码区间。

下面把这个问题讲透。


1️⃣ 为什么会有人觉得“必须放在作用域最后”?

很多人看到这种写法:

std::lock(m1, m2);

std::lock_guard<std::mutex> lg1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lg2(m2, std::adopt_lock);

// 临界区

于是误以为:

“哦,lock_guard(adopt_lock) 好像只能在最后写?”

其实不是。


2️⃣ 正确理解:不是“最后”,而是“之后”

✅ 正确顺序(唯一硬性要求)

m.lock();   // 或 std::lock(...)
std::lock_guard<std::mutex> lg(m, std::adopt_lock);

只要满足:

  • lg 构造时
  • m 已经被当前线程锁住

👉 放在作用域哪一行都行。


3️⃣ 举几个对比示例

✅ 合法:不是最后一行

void f() {
    std::lock(m);

    int x = calc();   // 还没用到锁也没关系

    std::lock_guard<std::mutex> lg(m, std::adopt_lock);

    // 临界区
}

完全 OK。


❌ 错误:在加锁之前构造

void f() {
    std::lock_guard<std::mutex> lg(m, std::adopt_lock); // ❌ UB
    std::lock(m);
}

问题:

  • lg 析构时会 unlock
  • 但你根本没 lock 过
  • 👉 未定义行为

4️⃣ 真正需要你关注的是「作用域覆盖范围」

lock_guard 的生命周期 = 锁的持有时间

你想锁多久,就让对象活多久

{
    std::lock(m);
    std::lock_guard<std::mutex> lg(m, std::adopt_lock);

    critical();   // 受保护
}
// 这里自动 unlock

如果你提前离开作用域,锁就提前释放

{
    std::lock(m);
    std::lock_guard<std::mutex> lg(m, std::adopt_lock);
}
// 锁已释放

other(); // 不再受保护

5️⃣ 常见“看起来像必须放最后”的原因

原因 1:RAII 的可读性习惯

很多团队习惯:

lock();
lock_guard(...);
// 临界区

只是为了 让读代码的人一眼明白:从这里开始持锁


原因 2:避免“空窗期”

不推荐这样:

std::lock(m);
// ……很多代码……
std::lock_guard<std::mutex> lg(m, std::adopt_lock);

因为中间如果:

  • return
  • throw
  • goto

👉 锁会泄漏


6️⃣ 所以真正的最佳实践是?

✅ 紧挨着写(不是“最后”,而是“立刻”)

std::lock(m1, m2);
std::lock_guard<std::mutex> lg1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lg2(m2, std::adopt_lock);

// 临界区

理由:

  • 不留异常/return 空窗
  • 生命周期清晰
  • 不容易被误用

7️⃣ 现代 C++:一行解决所有问题(强烈推荐)

如果你能用 C++17:

std::scoped_lock lock(m1, m2);
  • 没有 adopt_lock
  • 没有顺序问题
  • 没有作用域歧义
  • 几乎不可能写错

8️⃣ 一句话总结(记住这个)

lock_guard(m, adopt_lock) 不是必须放在作用域最后
必须在锁已被当前线程持有之后立刻构造,并让它的作用域覆盖临界区

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐