C++ volatile keywords and common misunderstanding summary

  • 2020-06-07 04:54:30
  • OfStack

preface

Recently, I saw the definition of the volatile keyword in the C++ standard, and found that it is completely different from the volatile keyword in java. The C++ volatile has little help for concurrent programming. The Internet has also seen a lot of misconceptions about volatile, so I decided to write this article to explain in detail what exactly volatile is for.

Why volatile?

The volatile keyword in C/C++ corresponds to const and is used to modify variables, usually to establish language level memory barrier. Here is the description of the volatile modifier in "The C++ Programming Language" :

[

A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

]

The volatile keyword is a type modifier that declares type variables that can be changed by factors unknown to the compiler, such as operating system, hardware, or other threads. When a variable is declared by this keyword, the compiler no longer optimizes the code that accesses it, thus providing stable access to a particular address. int volatile vInt; When the value of a variable declared by volatile is required, the system always rereads the data from the memory in which it is located, even though the previous instruction has just read the data from there. And the read data is saved immediately.

Compiler optimization of code

Before we get to the volatile keyword, let's talk about compiler optimization.


int main() {
 int i = 0;
 i++;
 cout << "hello world" << endl;
}

According to the code, the program will reserve the size of int in memory, initialize this memory to 0, then add 1 to the data in this memory, and finally output "hello world" to standard output. But a program compiled from this code (plus the -O2 option) does not reserve int-sized memory space, let alone add 1 to the number in memory. He will only print "hello world" to standard output.

In fact, it is not difficult to understand this is the compiler to optimize the code, modify the logic of the program. In fact, the C++ standard allows you to write code that is not identical to the actual generated program. Optimizing your code is a good thing, but you can't let the compiler modify the logic, or you won't be able to write reliable programs. So there is a limit to how C++ can rewrite this logic. The limit is that after the compiler modifies the logic, the program will remain unchanged to the external IO. How do you understand that? We can actually view the program that we write as a black box, and if we put the same inputs in the same order, it's going to give the same output in the same order every time. I/O here includes standard I/O, file system, network IO, even some system call, and so on, everything outside the program. So for the user of the program, as long as the input and output of the two black boxes are exactly 1, then the two black boxes are 1, so the compiler can rewrite the logic of the program at will under this limitation.

The role of the volatile keyword

I don't know if you noticed, but when I mentioned I/o, I didn't mention memory, in fact, what the program does to its own memory doesn't belong to external I/O. This is why, in the example above, the compiler can remove operations on the i variable. Sometimes the operating system maps hardware to memory and lets the program manipulate the hardware by performing operations on memory, such as mapping disk space to memory. Then the operation on this part of memory actually belongs to the input and output outside the program. The operation of this part of memory cannot be modified randomly, let alone ignored. This is where the volatile comes in handy. According to the C++ standard, the volatile variable of glvalue is operated like other input and output, and the order and content cannot be changed. This is like thinking of the operation on volatile as an input/output outside of the program. (glvalue is one of the value categories, which simply means objects that have space allocated in memory. See my other article for more details.)

According to THE C++ standard, this is the only function of volatile, but in some compilers (e.g., MSVC), volatile also has thread synchronization, but this is an extension of the compiler itself and cannot be used across platforms.

Common misconceptions about volatile

In fact, "volatile can synchronize between threads" is another common misconception. Take the following example:


class AObject
{
public:
 void wait()
 {
 m_flag = false;
 while (!m_flag)
 {
  this_thread::sleep(1000ms);
 }
 }
 void notify()
 {
 m_flag = true;
 }

private:
 volatile bool m_flag;
};

AObject obj;

...

// Thread 1
...
obj.wait();
...

// Thread 2
...
obj.notify();
...

People who have misconceptions about volatile, or who are not familiar with concurrent programming, may think that volatile guarantees that wait() reads m_flag and notify() writes m_flag, so Thread 1 wakes up normally. In fact, Thread 1 May never see m_flag become true. Because in multicore CPU, each CPU has its own cache. There is 1 part of the data in memory in the cache. When CPU wants to read and store the memory, it will first operate the cache instead of directly operating the memory. So the data in memory that multiple CPU "see" is different, which is called the memory visibility problem (memory visibility). Under concurrent programming, a program can have multiple threads running simultaneously in different CPU cores, at which time memory visibility will affect the correctness of the program. In this example, Thread 2 modified the memory corresponding to m_flag, but Thread 1 was running on other CPU cores, and the two CPU caches and memory were not synchronized. As a result, Thread 1 was running on a core that saw old data, and Thread 1 never woke up. Memory visibility is not the only problem encountered in multithreaded environments, and there are limits to what volatile can do with out-of-order execution. This is all part of concurrent programming, and I'm not going to expand on it here, but the volatile keyword is basically useless for concurrent programming.

So instead of using volatile, how should we modify the above example? C++11 started with a nice library called atomic class templates < atomic > In the header file, it is safe for multiple threads to access atomic objects. The following is the modified code:


class AObject
{
public:
 void wait()
 {
 m_flag = false;
 while (!m_flag)
 {
  this_thread::sleep(1000ms);
 }
 }
 void notify()
 {
 m_flag = true;
 }
private:
 atomic<bool> m_flag;
};

Just replace "volatile bool" with "atomic" < bool > "Yes. < atomic > The header file also defines several common aliases, such as "atomic < bool > "Can be replaced with" atomic_bool ". The atomic template overloads the usual operator, so atomic < bool > It is not very different from the normal bool variable. Some of the advanced USES of atomic, as it involves the memory model and concurrent programming of C++, I will not expand on them here, and I will have time to cover them later.

conclusion


Related articles: