20.3.2.1 Sharing Data Between Different Threads of Execution
Sharing data between different threads of execution (such as between mainline code and an interrupt service routine or between two different threads in a Realtime Operating System like environment) can sometimes be complex.
Make sure that any objects that may be shared in this way are marked as
volatile
(both read or write sharing). volatile
instructs the compiler to honor all accesses to memory, which will prevent the compiler
from caching a value in a register. If the variable is shared, then this is a good
thing! The compiler needs to know that the variable might change because of a hardware
or other external event, such as in this example where we wish to wait for the buffer to
have some data in it before progressing:
IOPORT.buffer_emtpy = 1;
while (IOPORT.buffer_empty);
If the object is not marked as volatile
when optimizing
the compiler, then it might determine that the value will never change and do something
horrible to the loop or worse. Consider the rest of the code to be unreachable and
replace the expression with while(1);
.
There are times however, when volatile
is not enough.
The compiler may not be able to compute an expression without going through an
intermediate register. This means there may be a window of time when a value is stored
in a register while the new value is being computed. If it is marked as
volatile
, then it will be written back. This could be the source of
data corruption, especially if the object is a single memory location that has many
separate data values like a C bitfield. In the following structure example,
status
is a flag set by some external process and
blinky
is a heartbeat in the mainline code.
volatile struct corruptable {
uint16_t status:3;
uint16_t blinky:1;
} object;
...
while (object.status != 0) {
object.blinky ^= 1;
}
If the compiler has not been able to generate an atomic, uninterruptable
sequence to XOR blinky
then this can be a possible source of
corruption. Consider the flow where status
is updated but the
blinky
update is not complete. Writing back the new value of
blinky
, which shares a word with status
, might
over-write the possibly new value of status
causing the generated code
to never see when status
has been updated.
If your code is similar to the above example, you can see that
volatile
is not a sufficient solution. Consider coding styles that
will prevent this overwrite from occurring, such as not sharing memory in that way and
the use of critical-sections to control access of shared data. Often it is efficient and
clearer to make use of the object-oriented principal of accessor functions where the
access of each object is tightly controlled in one place. A well-defined gating of
shared data can allow the code to be written without using volatile
at
all, thus allowing the code to be safe and share data efficiently.