I know, it has been made quite clear in a couple of questions/answers before, that volatile
is related to the visible state of the c++ memory model and not to multithreading.
On the other hand, this article by Alexandrescu uses the volatile
keyword not as a runtime feature but rather as a compile time check to force the compiler into failing to accept code that could be not thread safe. In the article the keyword is used more like a required_thread_safety
tag than the actual intended use of volatile
.
Is this (ab)use of volatile
appropriate? What possible gotchas may be hidden in the approach?
The first thing that comes to mind is added confusion: volatile
is not related to thread safety, but by lack of a better tool I could accept it.
Basic simplification of the article:
If you declare a variable volatile
, only volatile
member methods can be called on it, so the compiler will block calling code to other methods. Declaring an std::vector
instance as volatile
will block all uses of the class. Adding a wrapper in the shape of a locking pointer that performs a const_cast
to release the volatile
requirement, any access through the locking pointer will be allowed.
Stealing from the article:
template <typename T>
class LockingPtr {
public:
// Constructors/destructors
LockingPtr(volatile T& obj, Mutex& mtx)
: pObj_(const_cast<T*>(&obj)), pMtx_(&mtx)
{ mtx.Lock(); }
~LockingPtr() { pMtx_->Unlock(); }
// Pointer behavior
T& operator*() { return *pObj_; }
T* operator->() { return pObj_; }
private:
T* pObj_;
Mutex* pMtx_;
LockingPtr(const LockingPtr&);
LockingPtr& operator=(const LockingPtr&);
};
class SyncBuf {
public:
void Thread1() {
LockingPtr<BufT> lpBuf(buffer_, mtx_);
BufT::iterator i = lpBuf->begin();
for (; i != lpBuf->end(); ++i) {
// ... use *i ...
}
}
void Thread2();
private:
typedef vector<char> BufT;
volatile BufT buffer_;
Mutex mtx_; // controls access to buffer_
};
NOTE
After the first couple of answers have appeared I think I must clarify, as I might not have used the most appropriate words.
The use of volatile
is not because of what it provides at runtime but because of what it means at compile time. That is, the same trick could be pulled with the const
keyword if it was as rarely used in user defined types is as volatile
is. That is, there is a keyword (that happens to be spelled volatile) that allows me to block member function calls, and Alexandrescu is using it to trick the compiler into failing to compile thread-unsafe code.
I see it as many metaprogramming tricks that are there not because of what they do at compile time, but rather for what it forces the compiler to do for you.
I think the issue is not about thread-safety provided by volatile
. It dosen't and Andrei's article dosen't say it does. Here, a mutex
is used to achieve that. The issue is, whether the use of volatile
keyword to provide static type-checking along with use of mutex for thread-safe code, is abuse of the volatile
keyword? IMHO it's pretty smart, but i have come across developers who are not fans of strict-type-checking just for the sake of it.
IMO when you are writing code for multi-threaded environment, there is already enough caution to emphasize wherein you would expect people not to be ignorant of race-conditions and deadlocks.
A downside of this wrapped approach is that every operation on the type that is wrapped using LockingPtr
must be through a member function. That will increase one level of indirection which might considerably affect developers comfort in a team.
But if you are a purist who believes in the spirit of C++ a.k.a strict-type-checking; this is a good alternative.
This catches some kinds of thread-unsafe code (concurrent access), but misses others (deadlocks due to locking inversion). Neither is especially easy to test for, so it's a modest partial win. In practice, remembering to enforce a constraint that a particular private member is accessed only under some specified lock, hasn't been a big problem for me.
Two answers to this question have demonstrated that you're correct to say that confusion is a significant disadvantage - maintainers may have been so strongly conditioned to understand that volatile's memory-access semantics have nothing to do with thread-safety, that they will not even read the rest of the code/article before declaring it incorrect.
I think the other big disadvantage, outlined by Alexandrescu in the article, is that it doesn't work with non-class types. This might be a difficult restriction to remember. If you think that marking your data members volatile
stops you using them without locking, and then expect the compiler to tell you when to lock, then you might accidentally apply that to an int
, or to a member of template-parameter-dependent type. The resulting incorrect code will compile fine, but you may have stopped examining your code for errors of this kind. Imagine the errors which would occur, especially in template code, if it was possible to assign to a const int
, but programmers nevertheless expected the compiler would check const-correctness for them...
I think the risk that the data member's type actually has any volatile
member functions should be noted and then discounted, although it might bite somebody someday.
I wonder if there's anything to be said for compilers providing additional const-style type modifiers via attributes. Stroustrup says, "The recommendation is to use attributes to only control things that do not affect the meaning of a program but might help detect errors". If you could replace all mentions of volatile
in the code with [[__typemodifier(needslocking)]]
then I think it would be better. It would then be impossible to use the object without a const_cast
, and hopefully you wouldn't write a const_cast
without thinking about what it is you're discarding.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With