Yahoo Groups archive

AVR-Chat

Index last updated: 2026-04-28 22:41 UTC

Message

Volatile modifier (was: gcc compiler bad behaviour)

2012-04-15 by bayramdavies

I believe that I have considered these issues in enough depth to weigh in here and try to sort out this dispute.  We are talking here about the use of the volatile modifier, so I have changed the discussion subject accordingly.

Don Kinzer is correct to write that "The volatile attribute must be used on any ... variables modified by another thread/task/process that may run at any time" (with the implicit assumption that you also want to access them outside that thread).  We could really roll up Don's examples (which he did not, by the way, claim to be exhaustive) into:
- hardware registers, and
- variables modified and inspected in different threads of execution
if we define "threads of execution" to include the main thread, interrupts, exceptions, pre-emptive threads running on the same machine and all threads running on different machines that can access the same storage.

Bob Paddock is incorrect to assert that "Volatile is never the solution to a problem with threads", whether by "problem" we are talking about a normal design challenge or something that has gone wrong.  The use of volatile variables to communicate between threads is a perfectly valid design technique and, if your threads are misbehaving, then the lack of a volatile qualifier somewhere should definitely be checked.

Calling on references for this discussion does not work because John Regehr and Arch Robison are also wrong (although Arch may have been taken out of context).  Volatile is *extremely useful* for multi-threaded programming.  It is idiotic to state otherwise.  Don generously writes "We may be arguing different points", so what could Bob (with support from an Associate Professor of Computer Science at the University of Utah and the architect of Threading Building Blocks at Intel) be arguing about?  He would be correct to argue that the use of the volatile qualifier is not always a sufficient technique to ensure the orderly passage of data between threads.  You also need to ensure atomicity and to worry about thread synchronization.  But Don has already mentioned this.  Possibly, Bob is trying to say that the simple use of the volatile qualifier, without explicit execution barriers and handling of atomicity, is *never* a workable solution (this appears to be John Regehr's conjecture).  In this he would also be incorrect.

The definition of the volatile qualifier in the C language specification is in terms of the "virtual machine" and "sequence points" and how the implementation of a compiler may, for reasons of efficiency, deviate from the virtual machine as long as the result is "the same".  This is all incomprehensible to most of us, so we work with folk-lore definitions, such as the one provided by Don, and get into arguments like this one (and worse).  The only thing missing from Don's treatment of the subject is the importance of the volatile qualifier when modifying a storage location (Don only writes about using a variable that can be modified elsewhere).  It is good practice to make sure that any variable you write to that can be read elsewhere is declared volatile.  Not doing this is far less likely to cause a problem, as the compiler is unlikely to optimize away a write, but the modifier serves as useful documentation.  It may also be essential when dealing with a hardware register that is locally defined.  If the compiler can determine that the definition of the variable is invisible outside of the translation unit and it cannot see any reads of the variable, it may remove writes, thus eliminating the hardware side-effect you are trying to achieve.  So, Don's rule is a good one.  Use volatile with hardware registers and variables modified and inspected in different threads, with a suitably broad definition of threads.

I will now give an example of passing data between threads of execution using *only* the volatile qualifier, without synchronization primitives.  Data is queued in a buffer supported by read and write access methods and also by unsigned read and write counters.  The counters must be at least as large as the number of entries in the buffer and must be of a type such that access by the source and sink of data is inherently atomic (for example, they are one memory word).  The data source is allowed to insert data into the buffer using the write access method and may modify the write count but may only examine the read count.  To find out whether there is space in the buffer for new data, it takes the unsigned difference between the write and read counts (modulo the count size) and if the result is smaller than the buffer size by the data size, it goes ahead and inserts the data and updates the write count (using unsigned arithmetic, ignoring overflow).  The data sink is allowed to remove data from the buffer using the read access method and may modify the read count but may only examine the write count.  To find out whether there is data in the buffer, it takes the unsigned difference between the read and write counts and if the result is non-zero, it goes ahead and removes the data and updates the read count.  Note that the buffer access methods and their control variables are not shared; in fact source and sink have no idea how the other actually accesses the data.  The only information shared between the threads are the counts.  Only the source is permitted to modify the write count and only the sink can modify the read count.  If one reads the count written by the other at around the time it is being written, it will either get the value before the write, in which case it won't see the new data this time around, or it will get the value after the write, in which case it will see the new data.  Both are valid outcomes.  The integrity of the data passing is ensured by good algorithm design, not by indifferent algorithm design and fancy thread synchronization.

My operating system (ECROS) uses precisely the above method in the implementation of buffered serial communication.  The buffers are circular buffers and the access methods are read and write indexes into the buffer (which can actually be implemented by taking the read and write counts modulo the buffer size).  For a more complicated buffer, such as a linked list, it is important to remember that the read and write access methods must not share any control variables and must not modify a buffer entry that they don't "own" (are not, respectively, in the process of removing or adding).

Graham Davies
ECROS Technology
www.ecrostech.com

Attachments

Move to quarantaine

This moves the raw source file on disk only. The archive index is not changed automatically, so you still need to run a manual refresh afterward.