24

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.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • 2
    They are discussing that code currently in comp.lang.c++.moderated. – Johannes Schaub - litb Mar 22 '10 at 20:12
  • 2
    @Johannes: Is that a discussion they have to have twice each decade? I remember the heated discussions that came up when Andrei published this article. Like here, most of the heat was caused by people just reading `volatile` and "threads" in the same article, and jumping on it without even trying to understand what the idea was all about. – sbi Apr 05 '11 at 07:29

8 Answers8

6

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 volatilekeyword 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.

Abhay
  • 7,092
  • 3
  • 36
  • 50
  • 1
    +1. I do not agree with the extra level of indirection discomfort. Anyone doing multithreading must surely know that locking a mutex is the costly operation there. An extra level of indirection will amount to almost nothing compared to the lock itself. – David Rodríguez - dribeas Mar 22 '10 at 12:15
  • 1
    @dribeas: I meant syntactical discomfort. Believe me, some developers do not like to write wrapped-code instead of a *natural-to-the-eyes* simple direct code. – Abhay Mar 22 '10 at 12:19
4

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.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • There are a couple of interesting pearls in this answer: depending on a partial solution will probably make you forget about the cases where that will not help. Another good one is the potential use of attributes (even if I fail to see how to actually play the trick out of just attributes, it would require some type of `attribute_cast`). I had thought about classes that have `volatile` member functions but not having seen any in actual code I wondered how far reaching the problem would be. Thanks – David Rodríguez - dribeas Mar 22 '10 at 14:17
  • 3
    +1. He hacks the C++ type system to give a new meaning to `volatile`. As a side note: casting away `volatile`, as in `LockingPtr(volatile T& obj, Mutex& mtx) : pObj_(const_cast(&obj)), pMtx_(&mtx)` is undefined behavior according to section 6.7.3(5) of the standard, isn't it? – stephan Mar 22 '10 at 14:25
  • 1
    @dribeas: well, in the definition of `[[__typemodifier(X)]]` which I just invented in my head, `const_cast` can remove any type modifier, including user-invented ones, just as it currently removes `const` and `volatile`. Since user-defined type modifiers would have no meaning in the language other than preventing incompatible assignments, this is "safe" provided you don't use the result in a way which violates the invariants implied by the modifier in this program. I have not subjected this idea to thorough peer review, or indeed thought about it for longer than it took to type ;-) – Steve Jessop Mar 22 '10 at 14:43
  • 1
    @stephan: probably, but Alexandrescu was writing in 2001. It's now 2010, and it's still undefined behaviour to use threads *at all* (not long now, we hope!). You're at the mercy of your implementation for this stuff. Perhaps his article should have addressed that properly, but I can forgive that he let it go without saying. – Steve Jessop Mar 22 '10 at 14:45
  • @Steve: It is not undefined behavior to use threads. The standard says nothing about threads, not that they are undefined. Big difference. – John Dibling Mar 22 '10 at 18:42
  • @John: no difference at all from the standard's POV. Compiler extensions are undefined *by the standard*. An implementation does define the behaviour of threads, although sometimes not with the detail and the precision with which the standard defines behaviour. Then again, an implementation can (and often does) define the behaviour of dereferencing a null pointer, and yet we still say that the behaviour of such code is "undefined". – Steve Jessop Mar 22 '10 at 21:52
  • @Steve: Threads aren't compiler extensions any more than dialog boxes are. They are services provided by the operating system. – John Dibling Mar 22 '10 at 22:08
  • 3
    That's not true. Threads are not just a library. They require compiler support throughout the code which runs multithreaded, for example to ensure that memory accesses are not re-ordered across a memory barrier by some compiler transformation. This is the historical role of `volatile`: it prevents access being reordered across calls to unknown code. Dialog boxes don't require all code in the app to be compiled with special limits on behaviour (at least, not modal ones or fully asynchronous ones - dialog boxes with make callbacks from other threads are of course another matter). – Steve Jessop Mar 22 '10 at 22:27
  • Ah, found the article I wanted to link to in the first place: http://www.hpl.hp.com/techreports/2004/HPL-2004-209.html – Steve Jessop Mar 22 '10 at 22:33
  • 1
    "it prevents access being reordered across calls to unknown code". Sorry, that's not quite right, `volatile` prevents accesses being reordered across *each other*, and prevents the object from having values written to it that don't appear in the code (e.g. using the memory as temporary storage for something else if the compiler knows it will be unconditionally overwritten before the next read), so is part of one possible road to a thread-safe memory model in C. Playing such tricks across calls to unknown code is generally prevented anyway, because of the possibility of aliasing. – Steve Jessop Mar 22 '10 at 22:40
2

C++03 §7.1.5.1p7:

If an attempt is made to refer to an object defined with a volatile-qualified type through the use of an lvalue with a non-volatile-qualified type, the program behaviour is undefined.

Because buffer_ in your example is defined as volatile, casting it away is undefined behavior. However, you can get around that with an adapter which defines the object as non-volatile, but adds volatility:

template<class T>
struct Lock;

template<class T, class Mutex>
struct Volatile {
  Volatile() : _data () {}
  Volatile(T const &data) : _data (data) {}

  T        volatile& operator*()        { return _data; }
  T const  volatile& operator*() const  { return _data; }

  T        volatile* operator->()        { return &**this; }
  T const  volatile* operator->() const  { return &**this; }

private:
  T _data;
  Mutex _mutex;

  friend class Lock<T>;
};

The friendship is needed to strictly control non-volatile access through an already locked object:

template<class T>
struct Lock {
  Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); }
  ~Lock() { _data._mutex.unlock(); }

  T& operator*() { return _data._data; }
  T* operator->() { return &**this; }

private:
  Volatile<T> &_data;
};

Example:

struct Something {
  void action() volatile;  // Does action in a thread-safe way.
  void action();  // May assume only one thread has access to the object.
  int n;
};
Volatile<Something> data;
void example() {
  data->action();  // Calls volatile action.
  Lock<Something> locked (data);
  locked->action();  // Calls non-volatile action.
}

There are two caveats. First, you can still access public data members (Something::n), but they will be qualified volatile; this will probably fail at various points. And second, Something doesn't know if it really has been defined as volatile and casting away that volatile (from "this" or from members) in methods will still be UB if it has been defined that way:

Something volatile v;
v.action();  // Compiles, but is UB if action casts away volatile internally.

The main goal is achieved: objects don't have to be aware that they are used this way, and the compiler will prevent calls to non-volatile methods (which is all methods for most types) unless you explicitly go through a lock.

Fred Nurk
  • 13,952
  • 4
  • 37
  • 63
2

Building on other code and removing the need for the volatile specifier entirely, this not only works, but correctly propagates const (similar to iterator vs const_iterator). Unfortunately, it requires quite a bit of boilerplate code for the two interface types, but you don't have to repeat any logic of methods: each is still defined once, even if you do have to "duplicate" the "volatile" versions similarly to normal overloading of methods on const and non-const.

#include <cassert>
#include <iostream>

struct ExampleMutex {  // Purely for the sake of this example.
  ExampleMutex() : _locked (false) {}
  bool try_lock() {
    if (_locked) return false;
    _locked = true;
    return true;
  }
  void lock() {
    bool acquired = try_lock();
    assert(acquired);
  }
  void unlock() {
    assert(_locked);
    _locked = false;
  }
private:
  bool _locked;
};

// Customization point so these don't have to be implemented as nested types:
template<class T>
struct VolatileTraits {
  typedef typename T::VolatileInterface       Interface;
  typedef typename T::VolatileConstInterface  ConstInterface;
};

template<class T>
class Lock;
template<class T>
class ConstLock;

template<class T, class Mutex=ExampleMutex>
struct Volatile {
  typedef typename VolatileTraits<T>::Interface       Interface;
  typedef typename VolatileTraits<T>::ConstInterface  ConstInterface;

  Volatile() : _data () {}
  Volatile(T const &data) : _data (data) {}

  Interface       operator*()        { return _data; }
  ConstInterface  operator*() const  { return _data; }
  Interface       operator->()        { return _data; }
  ConstInterface  operator->() const  { return _data; }

private:
  T _data;
  mutable Mutex _mutex;

  friend class Lock<T>;
  friend class ConstLock<T>;
};

template<class T>
struct Lock {
  Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); }
  ~Lock() { _data._mutex.unlock(); }

  T& operator*() { return _data._data; }
  T* operator->() { return &**this; }

private:
  Volatile<T> &_data;
};

template<class T>
struct ConstLock {
  ConstLock(Volatile<T> const &data) : _data (data) { _data._mutex.lock(); }
  ~ConstLock() { _data._mutex.unlock(); }

  T const& operator*() { return _data._data; }
  T const* operator->() { return &**this; }

private:
  Volatile<T> const &_data;
};

struct Something {
  class VolatileConstInterface;
  struct VolatileInterface {
    // A bit of boilerplate:
    VolatileInterface(Something &x) : base (&x) {}
    VolatileInterface const* operator->() const { return this; }

    void action() const {
      base->_do("in a thread-safe way");
    }

  private:
    Something *base;

    friend class VolatileConstInterface;
  };

  struct VolatileConstInterface {
    // A bit of boilerplate:
    VolatileConstInterface(Something const &x) : base (&x) {}
    VolatileConstInterface(VolatileInterface x) : base (x.base) {}
    VolatileConstInterface const* operator->() const { return this; }

    void action() const {
      base->_do("in a thread-safe way to a const object");
    }

  private:
    Something const *base;
  };

  void action() {
    _do("knowing only one thread accesses this object");
  }

  void action() const {
    _do("knowing only one thread accesses this const object");
  }

private:
  void _do(char const *restriction) const {
    std::cout << "do action " << restriction << '\n';
  }
};

int main() {
  Volatile<Something> x;
  Volatile<Something> const c;

  x->action();
  c->action();

  {
    Lock<Something> locked (x);
    locked->action();
  }

  {
    ConstLock<Something> locked (x);  // ConstLock from non-const object
    locked->action();
  }

  {
    ConstLock<Something> locked (c);
    locked->action();
  }

  return 0;
}

Compare class Something to what Alexandrescu's use of volatile would require:

struct Something {
  void action() volatile {
    _do("in a thread-safe way");
  }

  void action() const volatile {
    _do("in a thread-safe way to a const object");
  }

  void action() {
    _do("knowing only one thread accesses this object");
  }

  void action() const {
    _do("knowing only one thread accesses this const object");
  }

private:
  void _do(char const *restriction) const volatile {
    std::cout << "do action " << restriction << '\n';
  }
};
Community
  • 1
  • 1
anon
  • 1
  • 1
1

Look at this from a different perspective. When you declare a variable as const, you are telling the compiler that the value cannot be changed by your code. But that doesn't mean that the value won't change. For example, if you do this:

const int cv = 123;
int* that = const_cast<int*>(&cv);
*that = 42;

...this evokes undefined behavior according to the standard, but in practice something will happen. Maybe the value will be changed. Maybe there will be a sigfault. Maybe flight simulator will launch -- who knows. The point is you don't know on a platform-independant basis what's going to happen. So the apparent promise of const is not fulfilled. The value may or may not actually be const.

Now, given that this is true, is using const an abuse of the language? Of course not. It is still a tool that the language provides to help you write better code. It will never be the end-all, be-all tool to ensure that values remain unchanged -- the programmer's brain is ultimately that tool -- but does that make const unuseful?

I say no, using const as a tool to help you write better code is not an abuse of the language. In fact I'd go one step further, and say it is the intent of that feature.

Now, the same is true of volatile. Declaring something as volatile will not make your program thread safe. It probably won't even make that variable or object thread safe. But the compiler will enforce CV-qualification semantics, and careful programmer can leverage this fact to help him write better code by helping the compiler to identify places where he might be writing a bug. Just like the compiler helps him when he tries to do this:

const int cv = 123;
cv = 42;  // ERROR - compiler complains that the programmer is potentially making a mistake

Forget about memory fences and atomicity of volatile objects and variables, just like you have long forgotten about cv's true constness. But use the tools that the language gives you to write better code. One of those tools is volatile.

John Dibling
  • 99,718
  • 31
  • 186
  • 324
0

You must better not do that. volatile was not even invented to provide thread-safety. It was invented to access memory-mapped hardware registers properly. volatile keyword has no effect over CPU's out-of-order execution feature. You should use proper OS calls or CPU defined CAS instructions, memory fences, etc.

CAS

Memory Fence

Malkocoglu
  • 2,522
  • 2
  • 26
  • 32
  • By just looking at the code provided, i see no gain making the BufT as volatile. The whole thread-safety thingie is Mutex's problem now. As BufT is private member, it should better be NOT volatile for performance reasons. But also I see that there is a problem with your code. Once the Thread1 locks Mutex, Thread2 will never gain access to BufT, so it will stuck at the first line of its body... – Malkocoglu Mar 22 '10 at 11:30
  • 2
    If you read the article and code carefully you will see that the variable, when in use, is not `volatile` (the qualifier is removed through a `const_cast`). It is just a compile time trick. The whole thread safety, as @Malkocoglu points out, is correctly handled through the mutex. I guess confusion is the big drawback of the approach – David Rodríguez - dribeas Mar 22 '10 at 12:21
  • -1: I hesitated downvoting this, but ultimately I did because volatile is not used in the article or this post in order to ensure out-of-order executions happen in a certian way. Its used in order to leverage the compiler's type system, so your post is not relevant to the question. – John Dibling Mar 22 '10 at 14:40
  • 1
    I hate long questions that have a misleading title ;-) – Malkocoglu Mar 26 '10 at 14:23
  • I tried as much as I could not to be misleading, but it is hard to write a proper title for this. I'll take suggestions if you have any. – David Rodríguez - dribeas Feb 02 '11 at 20:04
0

In the article the keyword is used more like a required_thread_safety tag than the actual intended use of volatile.

Without having read the article – why isn’t Andrei using said required_thread_safety tag then? Abusing volatile doesn’t sound such a good idea here. I believe this causes more confusion (like you said), rather than avoiding it.

That said, volatile may sometimes be required in multi-threaded code even if it’s not a sufficient condition, just to prevent the compiler from optimizing away checks that rely on asynchronous update of a value.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • The reason for using `volatile` instead of a tag is that the type system can use that at compile time to flag invalid accesses. Now that you mention this, I would have to think whether something based on a tag could work... – David Rodríguez - dribeas Feb 02 '11 at 19:50
  • @David: I think it should work but I readily admit that it may be more difficult to achieve. For one thing, you’d need to implement a smart-pointery interface. Perhaps. Haven’t really thought this through. – Konrad Rudolph Feb 02 '11 at 20:09
-2

I don't know specifically whether Alexandrescu's advice is sound, but, for all that I respect him as a super-smart dude, his treatment of volatile's semantics suggests that he's stepped way outside his area of expertise. Volatile has absolutely no value in multithreading (see here for a good treatment of the subject) and so Alexandrescu's claim that volatile is useful for multithreaded access leads me to seriously wonder how much faith I can place in the rest of his article.

Marcelo Cantos
  • 181,030
  • 38
  • 327
  • 365
  • 2
    I think you have misunderstood the article, the only reason that the `volatile` keyword is used is because of what it implies at compile time, not at runtime. During runtime, all uses (that compile) remove the `volatile` qualifier before operating on the instance: there is no `volatile` in the compiled code as all uses go through the `const_cast`-ed pointer inside the LockPtr which is non-volatile – David Rodríguez - dribeas Mar 22 '10 at 12:19
  • 1
    From the article you link to, "Hans Boehm points out that there are only three portable uses for volatile". Hans Boehm is mistaken (although not in a way he should be ashamed of) - Alexandrescu has presented a fourth use of volatile, which is to rely on its const-style contagious behaviour rather than on its semantics relating to memory access. – Steve Jessop Mar 22 '10 at 13:13
  • @David: The article leaves little room for interpretation: "It's intended to be used in conjunction with variables that are accessed and modified in different threads. Basically, without volatile, either writing multithreaded programs becomes impossible, or the compiler wastes vast optimization opportunities." I understand that the article's main thrust is the use of `volatile` as a kind of orthogonal const-qualifier. I never repudiated this. I merely expressed reservations about an article on `volatile` by someone who clearly didn't understand its original intent. – Marcelo Cantos Mar 22 '10 at 13:21
  • 2
    Alexandrescu is conflating two things: what `volatile` is defined to do, and how `volatile` was actually used by compiler-writers (he mentions MS in particular) adding threading to C and C++. Multithreading cannot be implemented merely as a library, it requires language support, and at least on Windows part of that language support came in the shape of additional semantics for `volatile`. With a coherent cache, volatile access is roughly the same as a memory barrier. Whether Alexandrescu blurred this distinction in ignorance or for simplicity, I do not know. – Steve Jessop Mar 22 '10 at 13:53