Generic <programming>: Volatile -- the volatile modifier, the best friend of multi-thread programmers, and let your compiler check race conditions for you)
Andrei Alexandrescu
I don't want to disrupt your mood, but this column targets the most terrible problem in multithreaded programming. If it is difficult to write an unexpected Security Program, as mentioned in the previous article generic <programming>, however, programs that are safe outside of the box are compared with multi-thread programming.
Multi-threaded programs are well known to be hard to write, difficult to verify, difficult to debug, difficult to maintain, and generally difficult to control. Incorrect multi-threaded programs may run for several years without any problems, but unexpected disasters may occur when certain conditions are met.
Needless to say, a programmer who writes multi-threaded Code needs all help. This topic focuses on race conditions-a common source of problems in multithreaded programs-to help you understand how to avoid them and provide them with tools, it will surprise you to see that you can enable the compiler to actively help you solve this problem.
It's just a small keyword.
Although the C and C ++ standards are clearly silenced by threads, they make a small concession to multithreading, which is represented by the volatile keyword.
As its more known partner const, volatile is a type modifier ).. It is used with variables to allow variables to be accessed and modified by different threads. Basically, if there is no volatile, either it is impossible to write a multi-threaded program, or the compiler wastes a great deal of optimization opportunities. Now let's explain why this is the case.
Consider the following code:
Class gadget
{
Public:
Void wait ()
{
While (! Flag _)
{
Sleep (1000); // sleep 1000 ms
}
}
Void wakeup ()
{
Flag _ = true;
}
...
PRIVATE:
Bool flag _;
};
The above gadget: Wait checks the flag _ member variable every second. If the variable is set to true by other threads, it is returned. At least this is the programmer's intention, but, alas, the wait function is wrong.
If the compiler determines that sleep (1000) is a call to the external library, and this call cannot modify the member variable flag _. Then the compiler will decide to cache the flag _ in the register and use that register to replace the slow memory. This is a good Optimization for single-threaded code, but in this case, this optimization damages the correctness: After you call wait for a gadget object, even though the other thread calls wakeup, wait will continue forever. This is because the modification to flag _ does not reflect the cached flag _ register. This optimization is indeed... over-optimized.
Caching variables in registers is a very useful Optimization in most cases, and it is a pity to waste it. C and C ++ give you the opportunity to explicitly disable this optimization. If you use volatile to identify a variable, the compiler will not cache the variable into the memory-each access to the variable will go directly through the actual memory location. Therefore, to make the wait/Wakeup of a gadget instance work normally, you only need to correct the flag _
Class gadget
{
Public:
... Same as above...
PRIVATE:
Volatile bool flag _;
};
Most of the explanations for the usage and usage of volatile end here, and it is recommended that you add the volatile identifier to the basic type in multithreading. However, you can do more with volatile, because it is part of the fantastic Type System of C ++.
Use volatile for user-defined types
You can not only add the volatile identifier before the basic type, but also before the user-defined type. In this case. Volatile modifies this type like const (you can also add const and volatile to the same type)
Unlike const, volatile has different effects on basic types and user-defined types. That is to say, unlike classes, the basic type still supports all their operations (adding, multiplication, assignment, and so on) after the volatile identifier is added .). For example, you can assign a non-volatile int to a volatile int, but you cannot assign a non-volatile object to a volatile object.
Let's illustrate how volatile acts on user-defined types.
Class Gadge
{
Public:
Void Foo () volatile;
Void bar ();
...
PRIVATE:
String name _;
Int State _;
};
...
Gadget regulargadget;
Volatile gadget volatilegadget;
If you think volatile does not work on objects, be prepared to be shocked.
Volatilegadget. Foo (); // the call to the volatile object is successful.
Regulargadget. Foo (); // successful. No problem in calling the volatile function for non-volatile objects
Volatilegadget. Bar (); // failed! You cannot call a non-volatile function on a volatile object.
It is easy to convert the unidentified type to the corresponding volatile object. However, you cannot change volatile back to unidentified. You must use cast:
Gadget & ref = const_cast <gadget &> (volatilegadget );
Ref. Bar (); // succeeded
A class with a volatile identifier can only access the subset of its interface, and a subset controlled by the class implementer. You can only use const_cast to obtain full access to the type interface. In addition, like const, volatile will pass from the class to its members (for example, volatilegadget. Name _ and volatilegadget. State _ are volatile variables)
Volatile, critical sections, and race conditions)
The simplest and most commonly used synchronization facility in multi-threaded programs is mutex. A mutext provides the basic functions of acquire and release. Once you call acquire in a thread, any other thread that calls acquire will be blocked. Later, when the thread calls release, a thread that was previously blocked by acquire will be released. In other words, with a mutex, only one thread can obtain the processor time between acquire call and release call. The Execution Code between an acquire call and a release call is itself a critical section. (Windows terminology is a bit confusing, because it calls mutex itself a critical section (critical section ). Although "mutext" is actually a mutex within the process range, it would be better to call them thread mutex and process mutx .)
Mutex is used to protect data and prevent actual state conditions. According to the definition, an actual state condition is generated when multiple threads determine how the data processing result is scheduled by the thread. When two or more threads compete to use the same data, the actual state condition appears. Because the thread may be interrupted at any point in time, the data being processed may be damaged or misjudged. As a result, data modification or sometimes reading must be carefully protected by the critical section. In object-oriented programming, this usually means that you store a mutex as a member variable in a class and use it when you access class data.
Experienced multi-threaded programmers may have yawned while reading the above two paragraphs, but the purpose of the two paragraphs is to provide a warm-up, because now we need to associate multi-threaded programming with volatile. We plot the intersection between the C ++ world and the thread semantic world to achieve this.
* Any thread outside the critical section can be interrupted by any other thread at any time without any control. Therefore, the variable accessed by multiple threads is volatile. This also maintains the original intention of volatile-to prevent the compiler from accidentally caching values that are immediately used by multiple threads.
* A mutex is defined in the critical section and can be accessed by only one thread. The result is that in a critical section, the Execution Code has the semantics of a Single-threaded environment. The variable used cannot be volatile-you can remove the volatile identifier.
In short, the data shared by multiple threads is volatile outside the critical section, and non-volatile in the critical section.
You lock a mutex to enter the critical section. You can use a const_cast to remove the volatile identifier. If you put these two operations together, we will establish a connection between the C ++ type system and the thread semantics of the application. We can let the compiler check the final state conditions for us.
Lockingptr
We need a tool to centralize A mutex acquisition operation and a const_cast. We will develop the lockingptr template class. You can use a volatile object OBJ and a mutex object MTR to initialize this template class. During the lifetime of the template class, a lockingptr keeps the content of the user's agent in use. At the same time, lockingptr provides access to the OBJ that removes volatile. This access is provided in a smart pointer mode through operator-> and operator. Execute const_cast in lockingptr. This conversion is semantically valid because lockingptr keeps mutex occupied during the lifetime.
First, we will define the skeleton of the mutex class used by lockingptr:
Class mutex
{
Public:
Void acquire ();
Void release ();
...
};
To use lockingptr, you must use the data structures and basic functions used by your operating system to implement mutex.
Lockingptr uses the controlled variable type as the template. For example, if you want to manage a widget, you can use a lockingptr <widget> to initialize it with a variable of the volatile widget type.
The definition of lockingptr is very simple. Lockingptr implements a relatively simple smart pointer. It only aims to combine a const_cast and a critical section.
Template <typename T>
Class lockingptr {
Public:
// Constructor/destructor
Lockingptr (volatile T & OBJ, mutex & CTX)
: Pobj _ (const_cast <t *> (& OBJ )),
Pcontent _ (& content-as-you-go)
{CTX. Lock ();}
~ Lockingptr ()
{Pctx _-> unlock ();}
// Simulate pointer Behavior
T & operator *()
{Return * pobj _;}
T * operator-> ()
{Return pobj _;}
PRIVATE:
T * pobj _;
Mutex * pmt _;
Lockingptr (const lockingptr &);
Lockingptr & operator = (const lockingptr &);
};
Despite its simplicity, lockingptr is very helpful for writing correct multi-threaded code. You should define objects shared by several threads as volatile and cannot use const_cast for them -- always use lockingptr automatic objects. Here is an example:
Suppose you have two threads that share a vector <char> object.
Class syncbuf {
Public:
Void thread1 ();
Void thread2 ();
PRIVATE:
Typedef vector <char> buft;
Volatile buft buffer _;
Mutex CTX _; // controls access to buffer _
};
In a thread function, you simply use a lockingptr <buft> to obtain controlled access to the buffer _ member variable:
Void syncbuf: thread1 (){
Lockingptr <buft> lpbuf (buffer _, CTX _);
Buft: iterator I = lpbuf-> begin ();
For (; I! = Lpbuf-> end (); ++ I ){
... Use * I...
}
}
These codes are both easy to write and easy to understand-you need to use buffer _ at any time, and you must create a lockingptr <buft> to point to it. Once you do this, you can use all the vecotr interfaces.