【问题标题】:Should mutexes be mutable?互斥体应该是可变的吗?
【发布时间】:2011-05-06 20:30:06
【问题描述】:

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

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

【问题讨论】:

  • 请参阅 here 了解我对 getter 的看法。
  • @San:确实,您经常需要计算或读取类的一些结果。这不是 getter:struct process { bool started() const; void start(); }; 既没有 setter 也没有 getter。只有方法。相反,struct employee { int get_salary() const; void set_salary(int); }; 让我呕吐。
  • @San:我不太理解那个例子,但这并不重要。我也一样,有时会写 getter。 (我什至写过 setter。)但是通常这暗示着设计是错误的。
  • @Marcin 我没有吹毛求疵。我在学习。很抱歉在免费网站上浪费了您的时间,这里有免费的空间来提出问题并获得免费的建议作为回报。我可以向谁结账? ;) 老实说,我应该把它分成一个新问题。对于阻塞您的回复,我深表歉意。我什至没有考虑过。
  • @Marcin:心存感激。这一定让您的问题在 SO 的首页上停留了几个小时。我可以看到您仍然没有更多答案,但我认为这是由于您提出问题的方式不清楚。正如我在之前的评论中所说,我认为互斥体中不需要 getter/setter,所以我不知道如何回答你的问题。发布(a)您的互斥锁的可能设计,以便我们知道我们在说什么。

标签: c++ mutex mutable


【解决方案1】:

[答案已编辑]

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

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

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

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

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

【讨论】:

  • 我是一个 POSIX 菜鸟。但是,我读到信号量是作为 futexes 实现的,它维护等待锁的进程的 FIFO。按值传递它是个好主意吗?
  • @San J. 你不按值传递互斥锁,而是按值传递要读取的属性。
  • Alexandre,第一行不是必须是lock<mutex> locker(the_mutex)吗? (并且有一些方法可以不必提供 mutex 模板参数。)
  • mutable 还有其他合理用途。缓存非平凡获取的结果是可变公平的一大类地方。
  • @VoidStar:这就是我所说的“记忆化”。
【解决方案2】:

隐藏的问题是:你将保护你的类的互斥锁放在哪里?

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

“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,而锁对象正在调用非 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 中constmutable 的“新”含义的介绍非常有启发性:

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

【讨论】:

  • 这当然是一个很好的解释,+1 来自我。不过,我只是不确定它是否甚至解决了 OP 的问题,因为他的问题太模糊了。
  • @sbi:谢谢!事实上,我也遇到了同样的问题,所以我觉得我必须添加一些上下文。 OP 的隐藏信息是他正在使用隐藏在他想要保护的类中的互斥锁,但没有说清楚(我误读了第一个答案,并认为 Alexandre C. 只建议使用外部互斥锁,当他写道在五行中我花了大约一本书来解释什么,所以我也应该受到责备)... :-) ...
  • 很好的解释。这正是我想要的。
  • 感谢 Herb Sutter 的视频!值得花时间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多