互斥对象应该是可变的吗?

不知道这是一个时尚问题,还是有什么硬性规定..。

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

25562 次浏览

[Answer edited]

Basically using const methods with mutable mutexes is a good idea (don't return references by the way, make sure to return by value), at least to indicate they do not modify the object. Mutexes should not be const, it would be a shameless lie to define lock/unlock methods as const...

Actually this (and memoization) are the only fair uses I see of the mutable keyword.

You could also use a mutex which is external to your object: arrange for all your methods to be reentrant, and have the user manage the lock herself : { lock locker(the_mutex); obj.foo(); } is not that hard to type, and

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

has the advantage it doesn't require two mutex locks (and you are guaranteed the state of the object did not change).

The hidden question is: where do you put the mutex protecting your class?

As a summary, let's say you want to read the content of an object which is protected by a mutex.

The "read" method should be semantically "const" because it does not change the object itself. But to read the value, you need to lock a mutex, extract the value, and then unlock the mutex, meaning the mutex itself must be modified, meaning the mutex itself can't be "const".

If the mutex is external

Then everything's ok. The object can be "const", and the mutex don't need to be:

Mutex mutex ;


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

IMHO, this is a bad solution, because anyone could reuse the mutex to protect something else. Including you. In fact, you will betray yourself because, if your code is complex enough, you'll just be confused about what this or that mutex is exactly protecting.

I know: I was victim of that problem.

If the mutex is internal

For encapsulation purposes, you should put the mutex as near as possible from the object it's protecting.

Usually, you'll write a class with a mutex inside. But sooner or later, you'll need to protect some complex STL structure, or whatever thing written by another without mutex inside (which is a good thing).

A good way to do this is to derive the original object with an inheriting template adding the mutex feature:

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

This way, you can write:

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

The problem is that it won't work because object is const, and the lock object is calling the non-const lock and unlock methods.

The Dilemma

If you believe const is limited to bitwise const objects, then you're screwed, and must go back to the "external mutex solution".

The solution is to admit const is more a semantic qualifier (as is volatile when used as a method qualifier of classes). You are hiding the fact the class is not fully const but still make sure provide an implementation that keeps the promise that the meaningful parts of the class won't be changed when calling a const method.

You must then declare your mutex mutable, and the lock/unlock methods 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 ;
}

The internal mutex solution is a good one IMHO: Having to objects declared one near the other in one hand, and having them both aggregated in a wrapper in the other hand, is the same thing in the end.

But the aggregation has the following pros:

  1. It's more natural (you lock the object before accessing it)
  2. One object, one mutex. As the code style forces you to follow this pattern, it decreases deadlock risks because one mutex will protect one object only (and not multiple objects you won't really remember), and one object will be protected by one mutex only (and not by multiple mutex that needs to be locked in the right order)
  3. The mutexed class above can be used for any class

So, keep your mutex as near as possible to the mutexed object (e.g. using the Mutexed construct above), and go for the mutable qualifier for the mutex.

Edit 2013-01-04

Apparently, Herb Sutter have the same viewpoint: His presentation about the "new" meanings of const and mutable in C++11 is very enlightening:

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