74

不确定这是一个风格问题,还是有硬性规定的问题......

如果我想尽可能保持公共方法接口为 const,但使对象线程安全,我应该使用可变互斥锁吗?一般来说,这是一种好的风格,还是应该首选非常量方法接口?请证明你的观点。

4

2 回答 2

66

隐藏的问题是:你把保护你的班级的互斥锁放在哪里?

总而言之,假设您要读取受互斥体保护的对象的内容。

“read”方法在语义上应该是“const”,因为它不会改变对象本身。但是要读取值,你需要锁定一个互斥体,提取值,然后解锁互斥体,这意味着互斥体本身必须被修改,这意味着互斥体本身不能是“const”。

如果互斥锁是外部的

然后一切正常。对象可以是“const”,互斥体不必是:

Mutex mutex ;

int foo(const Object & object)
{
   Lock<Mutex> lock(mutex) ;
   return object.read() ;
}

恕我直言,这是一个糟糕的解决方案,因为任何人都可以重用互斥锁来保护其他东西。包括你。事实上,你会背叛你自己,因为如果你的代码足够复杂,你就会对这个或那个互斥体究竟在保护什么感到困惑。

我知道:我是那个问题的受害者。

如果互斥锁是内部的

出于封装目的,您应该将互斥锁尽可能靠近它所保护的对象。

通常,您将编写一个内部带有互斥锁的类。但是迟早,您将需要保护一些复杂的 STL 结构,或者其他人编写的没有互斥锁的任何东西(这是一件好事)。

一个很好的方法是使用继承模板派生原始对象,并添加互斥锁功能:

template <typename T>
class Mutexed : public T
{
   public :
      Mutexed() : T() {}
      // etc.

      void lock()   { this->m_mutex.lock() ; }
      void unlock() { this->m_mutex.unlock() ; } ;

   private :
      Mutex m_mutex ;
}

这样,您可以编写:

int foo(const Mutexed<Object> & object)
{
   Lock<Mutexed<Object> > lock(object) ;
   return object.read() ;
}

问题是它不起作用,因为它object是 const,并且锁对象正在调用非常量lockunlock方法。

困境

如果您认为const仅限于按位 const 对象,那么您就完蛋了,必须回到“外部互斥解决方案”。

解决方案是承认const更多的是语义限定符(就像volatile用作类的方法限定符时一样)。您隐藏了类不完全的事实,但仍确保提供一个实现,该实现承诺在调用方法const时不会更改类的有意义部分。const

然后,您必须声明您的互斥锁是可变的,以及锁定/解锁方法const

template <typename T>
class Mutexed : public T
{
   public :
      Mutexed() : T() {}
      // etc.

      void lock()   const { this->m_mutex.lock() ; }
      void unlock() const { this->m_mutex.unlock() ; } ;

   private :
      mutable Mutex m_mutex ;
}

恕我直言,内部互斥体解决方案是一个很好的解决方案:一方面必须将对象声明为一个靠近另一个,另一方面又将它们聚合在一个包装器中,这最终是同一回事。

但是聚合具有以下优点:

  1. 它更自然(在访问之前锁定对象)
  2. 一个对象,一个互斥体。由于代码风格迫使您遵循此模式,因此它降低了死锁风险,因为一个互斥锁将只保护一个对象(而不是您不会真正记住的多个对象),一个对象将仅受一个互斥锁保护(而不是由需要以正确顺序锁定的多个互斥体)
  3. 上面的互斥类可以用于任何类

因此,让您的互斥锁尽可能靠近互斥锁对象(例如,使用上面的 Mutexed 构造),并选择mutable互斥锁的限定符。

编辑 2013-01-04

显然,Herb Sutter 也有同样的观点:他关于C++11 中const和的“新”含义的介绍mutable非常有启发性:

http://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/

于 2010-11-08T22:22:25.603 回答
38

[答案编辑]

基本上使用带有可变互斥体的 const 方法是一个好主意(不要顺便返回引用,确保按值返回),至少表明它们不会修改对象。互斥量不应该是const,将锁定/解锁方法定义为const是一个无耻的谎言......

实际上,这(和记忆)是我看到的mutable关键字的唯一合理用途。

您还可以使用对象外部的互斥锁:将所有方法安排为可重入的,并让用户自己管理锁:{ lock locker(the_mutex); obj.foo(); }输入起来并不难,并且

{
    lock locker(the_mutex);
    obj.foo();
    obj.bar(42);
    ...
}

具有不需要两个互斥锁的优点(并且您可以保证对象的状态没有改变)。

于 2010-11-08T19:38:06.603 回答