getter函数是否需要互斥锁?

时间:2010-02-12 14:31:28

标签: c++ multithreading locking mutex

我有一个可以从多个线程访问的类。 getter和setter函数都有锁。是否需要用于吸气功能的锁?为什么呢?

class foo {
public:
    void setCount (int count) {
        boost::lock_guard<boost::mutex> lg(mutex_);
        count_ = count;
    }

    int count () {
        boost::lock_guard<boost::mutex> lg(mutex_); // mutex needed?
        return count_;
    }

private:
    boost::mutex mutex_;
    int count_;
};

8 个答案:

答案 0 :(得分:14)

你能解决锁定的唯一方法就是你可以说服自己系统会在所有情况下以原子方式转移受保护的变量。如果由于某种原因无法确定,那么您将需要互斥锁。

对于像int这样的简单类型,您可能能够说服自己这是真的,这取决于体系结构,并假设它已正确对齐单指令传输。对于任何比这更复杂的类型,你将不得不拥有锁。

答案 1 :(得分:5)

互斥锁真的只保护单个int吗?它有所不同 - 如果它是一个更复杂的数据类型,你肯定需要锁定。

但如果它只是int,并且您确定int是原子类型(即,处理器不必执行两次单独的内存读取以将int加载到寄存器中),并且您已经对性能进行了基准测试并确定您需要更好的性能,那么您可以考虑从getter和setter中删除锁定。如果您这样做,请务必将int限定为volatile。并写一条评论,解释为什么你没有互斥保护,如果课程发生变化你会在什么条件下需要它。

另外,请注意您没有这样的代码:

void func(foo &f) {
  int temp = f.count();
  ++temp;
  f.setCount(temp);
}

无论您是否使用互斥锁,都不是线程安全的。如果你需要做类似的事情,互斥保护必须在setter / getter函数之外。

答案 2 :(得分:4)

如果你没有在getter周围有一个互斥体,并且一个线程正在读取它而另一个线程正在写它,你会得到有趣的结果。

答案 3 :(得分:2)

在你的情况下可能不是,如果你的cpu是32位,但是如果count是一个复杂的对象或者cpu需要多条指令来更新它的值,那么是的

答案 4 :(得分:1)

序列化对共享资源的访问 需要锁定。在您的特定情况下,您可能只使用atomic integer operations,但一般情况下,对于需要多个总线事务的较大对象,您需要锁定以确保读者总是看到一致的对象

答案 5 :(得分:1)

这取决于被锁定对象的确切实现。但是,一般来说,当其他人正在阅读(获取?)它时,你不希望有人修改(设置?)一个对象。防止这种情况的最简单方法是让读者锁定它。

在更复杂的设置中,锁的实现方式是任何数量的人都可以一次读取,但是没有人可以在任何人正在阅读时写入,并且没有人可以在写入时读取。

答案 6 :(得分:1)

它们真的很需要。

想象一下,如果您有一个类foo的实例完全是某些代码的局部实例。你有这样的东西:

{
    foo j;
    some_func(j); // this stashes a reference to j where another thread can find it
    while (j.count() == 0)
        bar();
}

假设优化器仔细查看了bar的代码,发现它不可能修改j.count_。这样,优化器可以按以下方式重写代码:

{
    foo j;
    some_func(j); // this stashes a reference to j where another thread can find it
    if (j.count() == 0)
    {
        while (1)
            bar();
    }
}

显然这是一场灾难。另一个线程可能会调用j.setCount(5),并且该线程不会退出循环。

编译器可以证明bar无法修改j.count()的返回值。如果需要假定另一个线程可以修改其访问的每个内存值,则它永远都不能将任何东西存放在寄存器中,这显然是站不住脚的情况。

因此,是的,需要锁定。另外,您需要使用其他提供类似保证的结构。

除非您确实没有其他实际选择,否则请不要编写依赖于编译器无法进行允许他们进行的任何优化的代码。在我编写程序的许多年中,我已经看到这会造成很多痛苦。今天的优化程序可以完成十年前被认为是荒谬的事情,而且许多代码的持续时间比您预期的更长。

答案 7 :(得分:0)

其他答案(特别是David Schwartz的答案)已经涵盖了同步问题。

尽管如此,我还没有解决另一个问题:这通常是一个糟糕的设计。

假设我们有foo的正确同步版本,请考虑David的示例代码。

{
    foo j;
    some_func(j);
    while (j.count() == 0)
    {
        // do we still expect (j.count() == 0) here?
        bar();
    }
}

while条件仍保留在体内的代码建议。毕竟,这就是单线程代码的工作方式。

但是,当然,即使我们正确同步了getter的实现,在我们继续执行while条件和执行循环主体的第一条指令之间,仍然可以从另一个线程调用setter。

因此,如果循环主体中的任何逻辑都不能依赖于条件为真,那么测试它的目的是什么?

有时候这很有意义,例如

while (foo.shouldKeepRunning())
{
    // foo event loop or something
}

如果我们的shouldKeepRunning状态在循环主体中发生变化,这是可以的,因为我们只需要定期对其进行测试。但是,如果要使用count 做某事,则需要一个寿命更长的锁,以及一个支持它的接口:

{
    auto guard = j.lock_guard();
    while (j.count(guard) == 0) // prove to count that we're locked
    {
        // now we _know_ count is zero in the body
        // (but bar should release and re-acquire the lock or that can never change)
        bar(j);
    }
} // guard goes out of scope and unlocks