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

Including std::lock_guard in extra scope

本文关键字:包含 范围内 guard std lock      更新时间:2023-10-16

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)

除了锁定持续时间短之外,还有其他优点吗?

什么可能是负面副作用?

是的,将锁保护的范围限制为尽可能短是有意义的,但不能更短

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

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

可能还有一点需要考虑(我在这里没有足够的实践经验来肯定地发言)。锁定/释放互斥体可能是一种性能成本极高的操作。因此,可能会发现,保持锁的时间稍长,而不是解锁&在一次操作过程中多次重新锁定它实际上可以提高整体性能。这是剖析可以向你展示的东西。

这可能有一个缺点:不能用这种方式保护初始化。例如:

{
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
};

这样,您可以保护检索过程,但初始化本身仍然没有受到保护。

是的,这是有道理的。

没有其他优点,也没有副作用(这是一个很好的写作方式)。

一个更好的方法是将其提取到一个私有成员函数中(如果您有一个以这种方式同步的操作,您还可以为该操作命名):

{
// 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对象的生存期确实是一种很好的做法。正如其他答案所指出的,将互斥锁锁定最短的时间将减少另一个线程阻塞互斥锁的机会。

我看到了另一个答案中没有提到的问题:事务操作。让我们用两个银行账户之间转账的经典例子。为了使您的银行程序正确,必须在不解锁之间的互斥锁的情况下修改两个银行帐户的余额。否则,当程序处于一种奇怪的状态时,另一个线程可能会锁定互斥锁,其中只有一个帐户被记入贷方/借方,而另一个帐户的余额没有受到影响!

考虑到这一点,当每个共享资源被修改时,仅仅确保互斥锁是不够的。有时,您必须将互斥锁锁定一段时间,该时间跨越所有形成事务的共享资源的修改。

编辑:

如果由于某种原因,在事务的整个持续时间内保持互斥锁是不可接受的,则可以使用以下算法:
1。锁定互斥,读取输入数据,解锁互斥
2。执行所有需要的计算,将结果保存在本地
3。锁定互斥锁,检查输入数据是否没有更改,执行具有现成结果的事务,解锁互斥锁。

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

我看不出有什么理由这么做。如果你做一些简单的事情,比如"设置一个变量"-使用atomic<>而且你根本不需要互斥和锁。如果您做了一些复杂的事情——将这些代码提取到新函数中,并在第一行使用lock。