在额外范围内包含 std::lock_guard

Including std::lock_guard in extra scope

std::lock_guard 放在额外的作用域中以使锁定期尽可能短是否有意义?

伪代码:

// all used variables beside the lock_guard are created and initialized somewhere else
...// do something

{ // open new scope
    std::lock_guard<std::mutex> lock(mut);
    shared_var = newValue;  
} // close the scope

... // do some other stuff (that might take longer)

除了锁定时间短,还有其他优势吗?

可能有哪些负面影响?

是的,将锁守卫的范围限制得尽可能短当然是有意义的,但不能更短。

持有锁的时间越长,线程阻塞等待该锁的可能性就越大,这会影响性能,因此通常被认为是一件坏事。

但是,您必须确保程序仍然是正确的,并且必须始终持有锁,即当访问或修改受锁保护的共享资源时。

可能还有一点需要考虑(我这里没有足够的实践经验来肯定地说)。 Locking/releasing 互斥锁本身可能是一项具有重要性能成本的操作。因此,事实证明,在一个操作过程中将锁保持稍长一段时间而不是多次解锁和重新锁定实际上可以提高整体性能。这是分析可以向您展示的内容。

是的,有道理。

没有其他优点,也没有副作用(写的很好)

更好的方法是将其提取到一个私有成员函数中(如果您有一个以这种方式同步的操作,您不妨给该操作起一个自己的名字):

{
    // all used variables beside the lock_guard are created and initialized somewhere else
    ...// do something

    set_var(new_value);

    ... // do some other stuff (that might take longer)
}

void your_class::set_value(int new_value)
{
    std::lock_guard<std::mutex> lock(mut);
    shared_var = new_value;
}

可能有一个缺点:您无法通过这种方式保护初始化。例如:

{
    std::lock_guard<std::mutex> lock(mut);
    Some_resource var{shared_var};
} // oops! var is lost

你必须像这样使用赋值:

Some_resource var;
{
    std::lock_guard<std::mutex> lock(mut);
    var = shared_Var;
}

这对于某些类型可能不是最优的,对于这些类型,默认初始化后赋值的效率低于直接初始化。此外,在某些情况下,您无法在初始化后更改变量。 (例如 const 个变量)


user32434999指出了这个解决方案:

// use an immediately-invoked temporary lambda
Some_resource var {
    [&] {
        std::lock_guard<std::mutex> lock(mut);
        return shared_var;
    } () // parentheses for invoke
};

这样,你可以保护检索过程,但初始化本身仍然不受保护。

使用额外的范围来专门限制 std::lock_guard 对象的生命周期确实是一个很好的做法。正如其他答案所指出的那样,将互斥锁锁定最短的时间将减少另一个线程阻塞互斥锁的可能性。

我看到还有一点在其他回答中没有提到:事务性操作。让我们使用两个银行账户之间汇款的经典示例。为使您的银行程序正确无误,必须修改两个银行帐户的余额,而无需解锁两者之间的互斥锁。否则,当程序处于一种奇怪的状态时,另一个线程可能会锁定互斥锁,其中只有一个帐户 credited/debited 而另一个帐户的余额未受影响!

考虑到这一点,在每个共享资源被修改时确保锁定互斥锁是不够的。有时,您必须在修改 所有共享资源 形成事务的时间段内保持互斥体锁定。

编辑:

如果出于某种原因无法在整个事务期间保持互斥量锁定,您可以使用以下算法:
1.锁定互斥量,读取输入数据,解锁互斥量。
2. 执行所有需要的计算,将结果保存在本地。
3. 锁定互斥锁,检查输入数据没有改变,执行交易并获得现成的结果,解锁互斥锁。

如果输入数据在步骤 2 的执行过程中发生了变化,则丢弃结果并使用新的输入数据重新开始。

我看不出这样做的理由。 如果你做一些像 "set one variable" 这样简单的事情 - 使用 atomic<> 并且你根本不需要互斥锁和锁。如果你做一些复杂的事情 - 将这段代码提取到新函数中并在其第一行使用锁。