There is a misconception that the volatile qualifier will provide the following desired properties necessary for a multithreaded program:
- Atomicity: indivisible memory operations
- Visibility: the effects of a write action by a thread are visible by other threads
- Ordering: Sequences of memory operations by a thread are guaranteed to be seen in the same order by other threads
Unfortunately, the volatile
qualifier does not provide guarantees for any of these, neither by definition nor by the way it is implemented in various platforms. For more information on how volatile is implemented, consult recommendation DCL17-C. Beware of miscompiled volatile-qualified variables.
The C99 standard, section The C Standard, subclause 5.1.2.3, paragraph 2 [ISO/IEC 9899:2011], says,
Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression may produce in general includes both value computations and initiation of side effects. At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete, and no side effects of subsequent evaluations shall have taken place.Value computation for an lvalue expression includes determining the identity of the designated object.
The volatile
keyword informs the compiler that the qualified In a nutshell, all this keyword does is to inform the compiler that this variable may change in ways that cannot be determined; thereforeconsequently, the compiler should not perform optimizations in these optimizations must be restricted for memory areas marked as volatile
. In other wordsFor example, the compiler should not store is forbidden to load the value in into a register and use the register, instead of doing expensive memory accessessubsequently reuse the loaded value rather than accessing memory directly. This concept is closely related relates to multithreading because , if incorrect caching of a shared variable is cached, a thread may change it, and the other threads may read may interfere with the propagation of modified values between threads, causing some threads to view stale data.
This property of the The volatile
keyword is sometimes confused as providing atomicity of a variable that is sometimes misunderstood to provide atomicity for variables that are shared between threads in a multithreaded program. A variable declared as volatile
is not cached in a register, leading to this confusion that it can be used safely as a synchronization primitive. When declared volatile
, the compiler does not re-order Because the compiler is forbidden to either cache variables declared as volatile
in registers or to reorder the sequence of reads and writes to any given volatile variable, many programmers mistakenly believe that volatile variables can correctly serve as synchronization primitives. Although the compiler is forbidden to reorder the sequence of reads and writes to that memory location. However, the compiler might re-order a particular volatile variable, it may legally reorder these reads and writes with those respect to reads and writes to other memory locations. This might result in non-atomic operations on the synchronization variable resulting in errorsreordering alone is sufficient to make volatile variables unsuitable for use as synchronization primitives.
Further, the volatile
qualifier lacks any guarantees regarding the following desired properties necessary for a multithreaded program:
- Atomicity: Indivisible memory operations.
- Visibility: The effects of a write action by a thread are visible to other threads.
- Ordering: Sequences of memory operations by a thread are guaranteed to be seen in the same order by other threads.
The volatile
qualifier lacks guarantees for any of these properties, both by definition and by the way it is implemented in various platforms. For more information on how volatile
is implemented, consult DCL17-C. Beware of miscompiled volatile-qualified variables.
Noncompliant Code Example
This noncompliant code example uses attempts to use flag
as a synchronization primitive.:
Code Block | ||||
---|---|---|---|---|
| ||||
bool flag = false;
void test() {
while (!flag) {
sleep(1000);
}
}
void wakeup(){
flag = true;
}
void debit(unsigned int amount){
test();
account_balance -= amount;
}
|
In this example, the value of flag
is used to determine if whether the critical section can be executed or not. Because the flag
variable is not declared volatile
, it may be cached in registers. Before the value in the register is written to memory, another thread might be scheduled to run, resulting in that thread reading stale data.
Noncompliant Code Example
This noncompliant code example uses flag
as a synchronization primitive but qualifies flag
as a volatile
type.:
Code Block | ||||
---|---|---|---|---|
| ||||
volatile bool flag = false;
void test() {
while (!flag){
sleep(1000);
}
}
void wakeup(){
flag = true;
}
void debit(unsigned int amount) {
test();
account_balance -= amount;
}
|
Declaring flag
as volatile
solves the problem of reading values being cached, which causes stale data , but still does not provide atomicity to be read. However, volatile flag
still fails to provide the atomicity and visibility guarantees needed for synchronization primitives to work correctly. The volatile
keyword does not promise to provide the guarantees needed for synchronization primitives.
Compliant Solution
This code uses a mutex to protect critical sections.:
Code Block | ||||
---|---|---|---|---|
| ||||
#include <pthread<threads.h> int account_balance; pthreadmtx_mutex_t flag; /* Initialize = PTHREAD_MUTEX_INITIALIZER;flag */ voidint debit(unsigned int amount) { pthread_mutexif (mtx_lock(&flag) == thrd_error) { return -1; /* Indicate error */ } account account_balance -= amount; /* Inside critical section */ if (mtx_unlock(&flag) == thrd_error) { return -1; /* Indicate error */ } pthread_mutex_unlockreturn 0; } |
Compliant Solution (Critical Section, Windows)
This compliant solution uses a Microsoft Windows critical section object to make operations involving account_balance
atomic [MSDN].
Code Block | ||||
---|---|---|---|---|
| ||||
#include <Windows.h> static volatile LONG account_balance; CRITICAL_SECTION flag; /* Initialize flag */ InitializeCriticalSection(&flag); int debit(unsigned int amount) { EnterCriticalSection(&flag); account_balance -= amount; /* Inside critical section */ LeaveCriticalSection(&flag); return 0; } |
Risk Assessment
Recommendation | Severity | Likelihood | Remediation Cost | Priority | Level |
---|
CON02-C | Medium | Probable | Medium | P8 | L2 |
Automated Detection
Tool | Version | Checker | Description | ||||||
---|---|---|---|---|---|---|---|---|---|
Parasoft C/C++test |
| CERT_C-CON02-a | Do not use the volatile keyword |
Related Guidelines
...
Key here (explains table format and definitions)
Taxonomy | Taxonomy item | Relationship |
---|---|---|
CERT C | CON01-CPP. Do not use volatile as a synchronization primitive | Prior to 2018-01-12: CERT: Unspecified Relationship |
Bibliography
[IEEE Std 1003.1:2013] | Section 4.11, "Memory Synchronization" |
[ISO/IEC 9899: |
...
2011] | Subclause |
...
5.1.2.3, "Program Execution" |
Bibliography
Open Group 2004] Section 4.11 "Memory Synchronization"
[MSDN] |
...