互斥锁是否应该是可变的?

84

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

如果我想尽可能地保持公共方法接口的const,但是又想使对象线程安全,那么我应该使用可变的互斥量吗?总的来说,这是好的编程风格,还是应该优先考虑非const的方法接口?请阐述你的观点。


2
请点击此处查看我对于getter的看法。 - sbi
3
@San: 的确,你经常需要计算或从类中读取一些结果。这不是一个getter:struct process { bool started() const; void start(); };既没有setter也没有getter,只有方法。相反,struct employee { int get_salary() const; void set_salary(int); };让我恶心。 - Alexandre C.
2
@San:我并不是很理解那个例子,但这并不重要。我也有时会写getter(甚至写过setter)。但通常这表明设计可能存在问题。 - sbi
2
@Marcin 我并不是在挑剔,我只是在学习。很抱歉在一个免费的网站上浪费了您的时间,这里提供免费的空间来提问并获得免费的建议。我应该把支票开给谁呢?;) 说真的,我应该把它分叉成一个新问题。对于阻塞您的回复,我深表歉意。我甚至没有想到过这一点。 - San Jacinto
2
@Marcin:感恩吧。这一定让你的问题在SO的首页上停留了几个小时。我看到你仍然没有更多的答案,但我认为这是由于你表述问题的方式不够清晰。正如我在早些时候的评论中所说,我认为在互斥锁中没有必要使用getter/setter,因此我不知道如何回答你的问题。发布(一个)可能的互斥锁设计,以便我们知道我们正在谈论什么。 - sbi
显示剩余15条评论
2个回答

74
隐藏的问题是:你在哪里放置保护类的互斥量?
总之,假设您想读取由互斥量保护的对象的内容。
“读”方法在语义上应为“const”,因为它不会改变对象本身。但是要读取值,需要锁定互斥量,提取值,然后解锁互斥量,这意味着互斥量本身必须被修改,也就是说互斥量本身不能是“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”是常量,而锁定对象正在调用非常量的“lock”和“unlock”方法。
困境:
如果您认为“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/


这绝对是一个很好的解释,我给它加上一个赞。但我不确定它是否真正解决了提问者的问题,因为他的问题太模糊了。 - sbi
2
@sbi:谢谢!事实上,我也遇到了同样的问题,所以我觉得我必须添加一些上下文。OP 的隐藏信息是他正在使用一个在他想要保护的类中隐藏的互斥锁,但并没有明确说明(我误读了第一个答案,认为 Alexandre C. 只建议使用外部互斥锁,当他用五行写出了我需要一本书或更多来解释的内容时,我也应该受到责备)... :-) ... - paercebal
3
谢谢那个Herb Sutter的视频!很值得一看。 - mattypiper
注意:有比你提到的简单外部锁更好的方法。您可以拥有一个外部锁,不允许对底层互斥量进行随意访问。例如,这篇关于boost的文章,介绍了内部和外部锁定 - jwd

40

[回答已编辑]

基本上,在可变互斥量中使用const方法是一个好主意(顺便说一下,不要返回引用,确保通过值返回),至少可以表示它们不会修改对象。互斥锁不应该是const,将lock / unlock方法定义为const是一种无耻的谎言......

实际上,这(以及备忘录模式)是我所看到的唯一合理使用mutable关键字的情况。

您也可以使用一个外部的互斥锁:安排所有方法都是可重入的,并让用户自己管理锁:{ lock locker(the_mutex); obj.foo(); }并不难打,而且......

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

相比于需要两个互斥锁的方法,它具有优势(并且您可以保证对象的状态未更改)。


1
我是一个POSIX新手。然而,我读到信号量被实现为futexes,它们维护了一个等待锁的进程FIFO。将其按值传递是个好主意吗? - San Jacinto
1
@San J. 你不需要通过值传递互斥锁,而是通过值传递你想要读取的属性。 - Alexandre C.
1
亚历山大,第一行不需要是lock<mutex> locker(the_mutex)吗?(而且有不提供mutex模板参数的方法。) - sbi
1
mutable还有其他合理的用法。缓存非平凡获取结果是一个巨大的领域,其中使用mutable是合理的。 - VoidStar
4
@VoidStar:我所说的“memoization”是指记忆化。 - Alexandre C.
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接