用额外的范围包围 std::lock_guard 以减少关键部分的大小是否有意义?

问题描述 投票:0回答:5

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)

除了锁定时间短之外,还有更多优点吗?

可能有哪些副作用?

c++ multithreading locking mutex
5个回答
25
投票

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

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

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

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


12
投票

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

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

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


1
投票

是的,很有道理。

没有其他优点,也没有副作用(这样写就很好了)

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

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

1
投票

专门使用额外的作用域来限制 std::lock_guard 对象的生命周期确实是一个很好的做法。正如其他答案所指出的,在最短的时间内锁定互斥体将减少另一个线程在互斥体上阻塞的机会。

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

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

编辑:

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

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


-1
投票

我不明白这样做的理由。 如果你做一些像“设置一个变量”这样简单的事情 - 使用atomic<>并且你根本不需要互斥锁和锁。如果你做了一些复杂的事情 - 将此代码提取到新函数中并在其第一行使用 lock 。

© www.soinside.com 2019 - 2024. All rights reserved.