Volatile is a good helper for compiling multi-thread programs

Source: Internet
Author: User
Tags posix

I didn't mean to mess up your mood, but in this column we will discuss the topic of multi-threaded programming. As mentioned in the previous generic, writing exception-safe programs is very difficult, but compared with writing multi-threaded programs, it is just a play.

Multi-threaded programs are hard to write, verify, debug, and maintain. Incorrect multi-threaded programs may run for many years without any errors until certain critical conditions are met.

Needless to say, programmers who write multi-threaded programs need to use all possible help. This topic will focus on race conditions, which is usually the root cause of various troubles in multithreaded programs-learn more about it and provide some tools to prevent competition. Surprisingly, we will let the compiler do its best to help you with these tasks.

Just a humble keyword.
Although the C and C ++ standards are clearly "silent" for threads, they are in the form of volatile keywords and indeed leave a little privilege for multithreading.

Like the more familiar const, volatile is a type modifier ). It is designed to modify variables accessed and modified by different threads. If there is no volatile, it will basically lead to the following results: either you cannot write multi-threaded programs, or the compiler loses the chance of a lot of optimization. Next we will explain it one by one.

Consider the following code:

Code:

Class gadget
{
Public:
Void wait ()
{
While (! Flag _)
{
Sleep (1000); // sleeps for 1000 milliseconds
}
}
Void wakeup ()
{
Flag _ = true;
}
...
PRIVATE:
Bool flag _;
};

In the code above, the purpose of the gadget: Wait is to check the flag _ member variable every second. This function will return only when the flag _ is set to true by another thread. At least this is the intent of the program author. However, this wait function is incorrect.
Assuming that the compiler finds that sleep (1000) is an external library function that does not change the member variable flag _, the compiler can determine that it can cache the flag _ in the register, in the future, you can access this register to replace the memory on the slow motherboard. This is a good Optimization for single-threaded code, but in this case, it damages the correctness of the program: After you call a gadget wait function, even if waeup is called by another thread, wait continues to loop. This is because the changes to flag _ are not reflected in the registers cached. The Optimization of the compiler is a little too much ...... Optimistic.

In most cases, caching variables in registers is a very valuable optimization method. If not, it is a pity. C and C ++ provide you with the opportunity to explicitly disable such cache optimization. If you declare that the variable uses the volatile modifier, the compiler will not cache the variable in the Register-the actual location of the variable in the memory will be accessed each time. In this case, the correct modification to the wait/Wakeup of the gadget is to add the correct modification to the flag:

Class gadget
{
Public:
... As ababove...
PRIVATE:
Volatile bool flag _;
};

Most of the explanations about the principles and usage of volatile end here, and we recommend that you use volatile to modify the native type variables used in multiple threads. However, you can use volatile to do more, because it is part of the magical C ++ system.

Use volatile for custom types
Volatile modifier can be used not only for native types, but also for custom types. In this case, the volatile modifier is similar to const (you can also use both const and volatile for a type ).

Unlike const, volatile has different functions for native and custom types. That is to say, when the native types are modified with volatile, they still support various operations (addition, multiplication, assignment, etc.), but this is not the case for the class. For example, you can assign a non-volatile int value to a volatile int, but you cannot assign a non-volatile object to a volatile object.

Let's take an example to illustrate how the custom type volatile works.
Code:

Class gadget
{
Public:
Void Foo () volatile;
Void bar ();
...
PRIVATE:
String name _;
Int State _;
};
...
Gadget regulargadget;
Volatile gadget volatilegadget;

If you think volatile has no effect on objects, you will be surprised.
Volatilegadget. Foo (); // OK, volatile fun called
// Volatile object
Regulargadget. Foo (); // OK, volatile fun called
// Non-volatile object
Volatilegadget. Bar (); // error! Non-volatile function called
// Volatile object!

It is common to convert from a type without volatile modification to the corresponding volatile type. However, like const, you cannot convert the volatile type to a non-volatile type in turn. You must use the type conversion OPERATOR:
Gadget & ref = const_cast <gadget &> (volatilegadget );
Ref. Bar (); // OK

A volatile-modified class only allows access to a subset of its interfaces. This subset is controlled by the class implementer. You can only use const_cast to access all interfaces of this type. In addition, like const, the volatile attribute of the class is passed to its members (for example, volatilegadget. Name _ and volatilegadget. State _ are also volatile variables ).

Volatile, critical section and competitive conditions
The simplest and most commonly used synchronization mechanism in multi-threaded programs is mutex (mutex object. A mutex only provides two basic operations: Acquire and release. Once a thread calls acquire, other threads will be blocked when acquire is called again. When this thread calls release, one and only one thread that is blocked in acquire will be awakened. In other words, for a given mutex, only one thread can obtain the processor time between acquire and release calls. The Code executed between an acquire and a release call is called a critical section ). (Windows terms may cause a bit of confusion, because Windows calls mutex itself a critical section, while windows mutex actually refers to the mutex between processes. It may be better if they are called thread mutex and process mutex respectively .)

Mutex is used to avoid data competition conditions. According to the definition, the so-called competition condition is such a situation: the effect of multiple threads on data depends on the scheduling sequence of threads. When two threads compete to access the same data, competition conditions will occur. Because a thread can interrupt other threads at any time, data may be damaged or incorrectly interpreted. Therefore, data modification operations and access operations in some cases must be protected by the critical section. In object-oriented programming, this usually means that you save a mutex in the member variable of a class and use this mutex when you access the status of this class.

The multi-thread programming experts have read the above two paragraphs and may have yawned, but their purpose is to provide a preparation exercise. Now we need to associate it with volatile. We will compare the C ++ type with the thread semantics.

In addition to a critical section, any thread can interrupt other threads at any time. This is not controlled, so variables accessed by multiple threads are easy to be completely changed. This is consistent with volatile's original intention [1]-So volatile needs to be used to prevent the compiler from inadvertently caching such variables.

In the critical section limited by a mutex, only one thread can enter. Therefore, the Code executed in the critical section has the same semantics as the single-threaded program. The controlled variables will not be accidentally changed-you can remove the volatile modifier.

In short, the data shared between threads is volatile outside the critical section, but not within the critical section.

You lock a mutex to enter a critical section, and then you use const_cast to remove a certain type of volatile modifier. If we can successfully put these two operations together, then we establish a connection between the C ++ type system and the thread semantics of the application. In this way, we can let the compiler help us detect the competition conditions.

Lockingptr
We need a tool for mutex obtaining and const_cast operations. Let's design a lockingptr class. You need to initialize it with a volatile object OBJ and a mutex object MTR. During the lifecycle of the lockingptr object, it ensures that the content-based drop-down (MTR) object is in the acquired state, and also provides access to the OBJ modified by volatile. Access to OBJ is similar to smart pointer, which is implemented through operator-> and operator. Const_cast is performed within lockingptr. This conversion is semantically correct because lockingptr always has mutex during its lifetime.

First, Let's define the mutex class framework that works with lockingptr:

Code:

Class mutex
{
Public:
Void acquire ();
Void release ();
...
};

To use lockingptr, you need to use the data structure and underlying functions provided by the operating system to implement mutex.
Lockingptr is a template that uses the type of the controlled variable as the template parameter. For example, if you want to control a widget, you need to write lockingptr <widget>.

The definition of lockingptr is very simple. It only implements a simple smart pointer. It focuses only on putting const_cast and critical section operations together.

Code:

Template <typename T>
Class lockingptr {
Public:
// Constructors/Destructors
Lockingptr (volatile T & OBJ, mutex & CTX)
: Pobj _ (const_cast <t *> (& OBJ )),
Pcontent _ (& content-as-you-go)
{CTX. Lock ();}
~ Lockingptr ()
{Pctx _-> unlock ();}
// 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 useful for compiling correct multi-threaded code. You should declare the objects shared between threads as volatile, but never use const_cast for them-you should always use the automatic object (automatic objects) of lockingptr ). Let's give an example.

For example, you have two threads that need to share a vector <char> object:

Code:

Class syncbuf {
Public:
Void thread1 ();
Void thread2 ();
PRIVATE:
Typedef vector <char> buft;
Volatile buft buffer _;
Mutex mt_; // controls access to buffer _
};

In a thread function, you simply need to use a lockingptr <buft> object to obtain controlled access to the buffer _ member variable:

Code:

Void syncbuf: thread1 (){
Lockingptr <buft> lpbuf (buffer _, CTX _);
Buft: iterator I = lpbuf-> begin ();
For (; I! = Lpbuf-> end (); ++ I ){
... Use * I...
}
}

This code is easy to write and easy to understand-whenever you need to use buffer _, you must create a lockingptr <buft> to point to it. After doing so, you can access all the interfaces of the vector.

The advantage of this method is that if you make an error, the compiler will point it out:
Code:

Void syncbuf: thread2 (){
// Error! Cannot access 'begin' for a volatile object
Buft: iterator I = buffer _. Begin ();
// Error! Cannot access 'end' for a volatile object
For (; I! = Lpbuf-> end (); ++ I ){
... Use * I...
}
}

You cannot access any function of buffer _ unless you have performed const_cast or use lockingptr. The difference between the two is that lockingptr provides a regular method to perform const_cast on a volatile variable.
Lockingptr has excellent expressiveness. If you only need to call a function, you can create an unknown temporary lockingptr object and use it directly:

Code:

Unsigned int syncbuf: size (){
Return lockingptr <buft> (buffer _, CTX _)-> size ();
}

Back to native type
We have seen how powerful volatile is to protect objects from uncontrolled access, and how lockingptr provides a simple and effective way to write thread-safe code. Now let's go back to the native type. Volatile has different effects on them.

Let's consider an example where multiple threads share an int variable.

Code:

Class counter
{
Public:
...
Void increment () {++ CTR _;}
Void decrement () {-- CTR _;}
PRIVATE:
Int CTR _;
};

If the increment and decrement are called in different threads, There is a bug in the code snippet above. First, CTR _ must be volatile. Second, even an operation that looks atomic, such as ++ CTR _, is actually divided into three stages. The memory itself has no operational function. When incremental operations are performed on a variable, the processor will:
Read variables into registers
Add 1 to the value in the register
Write the result back to memory
This three-step operation is called rmw (read-Modify-write ). In the modify phase of an rmw operation, most processors release the memory bus so that other processors can access the memory.
If another processor also performs rmw operations on the same variable at this time, we will encounter a race condition: the second write will overwrite the first value.

To prevent such a problem, you need to use lockingptr:

Code:

Class counter
{
Public:
...
Void increment () {++ * lockingptr <int> (CTR _, CTX _);}
Void decrement () {-- * lockingptr <int> (CTR _, CTX _);}
PRIVATE:
Volatile int CTR _;
Mutex mt _;
};

Now this code is correct, but the quality of this Code is worse than that of syncbuf. Why? For counter, the compiler will not generate a warning when you mistakenly access CTR _ directly (without locking it. Although CTR _ is volatile, the compiler can still compile ++ CTR _, although the generated code is definitely incorrect. The compiler is no longer your ally. You only have to pay attention to the competition conditions.
So what do you do? You can use a high-level structure to encapsulate native data and then use volatile for that structure. This is a bit self-contradictory. Modifying the native type with volatile is not a good practice, though this is what volatile originally expected!

Volatile member functions
So far, we have discussed classes with volatile data members. Now let's consider designing a class that will serve as part of a larger object and be shared among threads. Here, the volatile member functions can be of great help.

When designing a class, you only modify those thread-safe member functions with volatile. You must assume that the code outside will call the volatile member function anytime anywhere. Don't forget: Volatile is equivalent to free multi-threaded code with no critical section; non-volatile is equivalent to a single-threaded environment or within the critical section.

For example, you define a widget class, which implements the same operation using two methods-a thread-safe method and a fast and unprotected method.

Code:

Class widget
{
Public:
Void operation () volatile;
Void operation ();
...
PRIVATE:
Mutex mt _;
};

Note the overloading usage here. Now, users of widgets can call operation with consistent syntax. For volatile objects, thread security can be achieved, and for common objects, speed can be obtained. You must define the shared widget object as volatile.
When implementing the volatile member function, the first operation is usually to use lockingptr to lock this, and then the rest of the work can be handed over to a non-volatile function with the same name:

Code:

Void Widget: Operation () Volatile
{
Lockingptr <widget> lpthis (* This, CTX _);
Lpthis-> operation (); // invokes the non-volatile Function
}

Summary
When writing a thread program, using volatile will be very helpful to you. You must adhere to the following rules:

Declare all shared objects as volatile
Do not directly use volatile for native types
When defining a shared class, the volatile member function is used to indicate its thread security.
If you do this and use the simple general-purpose component lockingptr, you can write thread-safe code and greatly reduce your worries about competing conditions, because the compiler will worry about you, and diligently pointed out what went wrong for you.

Using volatile and lockingptr has produced great results in several projects I have participated in. The code is neat and easy to understand. I remember some deadlocks, but I would rather deal with deadlocks than competing conditions, because they are much easier to debug. In fact, those projects have never encountered any problems related to the competition conditions.

Thank you
We are very grateful to James kanze and Sorin jianu for their insightful comments.

Thank you
We are very grateful to James kanze and Sorin jianu for their insightful comments.

Appendix: Misuse of volatile? [2]
I have received a lot of feedback since I published the previous topic "generic <programming>: volatile-multithreaded programmer's best friend. Like doomed, most of the comments are private letters and complaints are sent to USENET newsgroup Comp. Lang. c ++. moderated and comp. Programming. threads. Later, I had a very long heated discussion. If you are interested in this topic, you can go and see it. Its title is "volatile, was: Memory visibility between threads .".

I know that I have learned a lot from this discussion. For example, the example of a widget at the beginning of the article is not very simple. In short, in many systems (such as POSIX-compatible systems), volatile modification is not required. In other systems, the program is still incorrect even if volatile is added.

One of the most important problems with volatile correctness is that it relies on POSIX-like mutex. If it is on a multi-processor system, it is not enough to rely solely on mutex-you must use memory barriers.

Another more philosophical problem is: strictly speaking, it is illegal to remove the volatile attribute of a variable through type conversion, even if the volatile attribute is added for volatile correctness. As Anthony Williams pointed out, it can be imagined that a system may put volatile data in a storage zone different from non-volatile data. In this case, address transformation may result in uncertain behavior.

Another criticism is that although volatile correctness can solve the competition conditions at a lower level, it cannot correctly detect high-level and logical competition conditions. For example, you have an mt_vector template class to simulate STD: vector. The member functions have been correctly corrected by thread synchronization. Consider this Code:

Volatile mt_vector <int> VEC;
...
If (! VEC. Empty ()){
VEC. pop_back ();
}

The purpose of this Code is to delete the last element in the vector, if it exists. He works well in a single-threaded environment. However, if you use it in a multi-threaded program, this Code may throw an exception even though empty and pop_back both have correct thread synchronization behaviors. Although the consistency of underlying data (VEC) is ensured, the results of high-level operations are still uncertain.
In any case, after the debate, I kept my suggestion that volatile correctness is a valuable tool for detecting competitive conditions on a POSIX-like mutex system. However, if you are on a multi-processor system that supports memory access re-sorting, you must first carefully read your compiler documentation. You must know yourself and yourself.

Finally, kenth Chiu mentioned a very interesting article http://theory.stanford.edu /~ I guess what the question is? "Type-based race detection for Java ". This article explains how to make a small supplement to the Java type system so that the compiler and the programmer can check the competition conditions during compilation.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.