Pan: volatile - multi-line programmer The best friend Volatile modifier and make your compiler for you to check the essence cond

xiaoxiao2021-03-06  68

Andrei alexandrescu

I don't want to destroy your emotions, but this column is the most terrible problem in multi-threaded programming. If it is, as the previous generic is difficult to write unified security (Exception-Safe) is difficult, but the procedures and multi-threaded programming are less than the childhood. Programs for multithreading are well known, difficult to verify, difficult to debug, difficult to maintain, in general, it is difficult to drive. Incorrect multi-threaded programs may not have problems a few years, but they will lead to unpredictable disasters when certain time conditions meet. Needless to say, a programmer written by multi-threaded code requires everything you can get. This column is centrally discussed in a multi-threaded program. Source - Let you know how to avoid it and provide you with tools, and you will surprise you to make your compiler to actively help you Handling this problem.

It's just a small keyword although C and C standards clearly keep the threads silent, they still have a small concession for multi-threads, which makes a volatile keyword. As its more known partner const, Volatile is a Type Modifier. . Its role is that the variable is used to enable variables to access and modify different threads. At all, if there is no volatile, it is either to write a multi-threaded program, or the compiler is wasting a great optimization opportunity. Now explain why this is this. Consider the following code:

Class gadget {public: void Wait () {while (! Flag_) {Sleep (1000); // Sleep 1000 millisecond}} void wakeup () {flag_ = true;} ... private: BOOL FLAG_;};

The role of gadget :: wait is to check the FLAG_ member variable per second, returns when that variable is set to True by other threads. At least this is the original intention of the programmer, but, hey, the wait function is wrong. If the compiler discsses the 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 FLAG_ in the register and replace slower memory with that register. This is very good to optimize the single-threaded code, but in this case, this optimization has destroyed correctness: After calling Wait on a gadget object, despite another thread to call Wakeup, Wait will always cycle Go down. This is because modifications to FLAG_ are not reflected to the registers of the cache FLAG_. This optimization is really ... over optimized. Caching variables into registers Most of the time is a very useful optimization, and it is a pity to waste. C and C give you the opportunity to explicitly disable this optimization. If you identify a variable with a volatile, the compiler will not cache that variable into the accumulator - each access to the variable is directly through the actual memory location. So let the gadget's wait / wakeup work properly as long as it is correctly modified FLAG_

Class gadget {public: ... is above ... private: Volatile Bool Flag_;

Most of the explanation of Volatile uses and usage is suggesting that you add a Volatile identifier for the basic type in the multi-thread. However, you can do more things with volatile because it is part of the C wonderful type system.

Using Volatile, you can use Volatile, you can add a Volatile identifier before the basic type, and can also be added before the user-defined type. under these circumstances. Volatile is modified as constrays (you can also simultaneously add const and volatile at the same time) but are unlike const, Volatile different types of basic types and user-defined types. That is to say, unlike class, the basic type is still supported after the Volatile identifier is still supporting all of them (plus, multiplied, assignment, etc.). For example, you can assign a non-Volatile Int to a Volatile Int, but you can't assign a non-Volatile object to a volatile object. Let us exemplify how Volatile acts on the user-defined type. Class gadge {public: void foo () volatile; void bar (); ... private: string name_; int state _;}; ... gadget regulagadget; volatile gadget volatilegadget;

If you think volatile doesn't work for objects, it is ready to be scared.

Volatilegadget.foo (); // Success, call the volatile object to call the volatile function without problem regulagadget.foo (); // Success, call the volatile function for non-Volatile objects No problem volatilegadget.bar (); // Failed! Can't call the volatile object to call non-Volatile functions

Converting the identified type to the corresponding Volatile object is simple. However, you can't turn the volatile to no logo. You must use CAST:

Gadget & ref = const_cast (volatilegadget); ref.bar (); // successful

A class with a Volatile identifier can only access the subset of its interface, a subset of control by the implementation of the class. Users can only use const_cast to get full access to the type interface. Also, like Const, Volatile will pass from class to its member (for example, volatilegadget.name_ and volatilegadget.state_ is volatile variable)

The easiest way to use the most synchronous facilities in the multi-threaded program of Volatile, Race Conditions, is MUTEX, a MUTEXT provides the basic features of Acquire and Release. Once you call acquire in a thread, any other call acquire is getting clogged. Sowing of the thread calling Release, there will be a thread that is previously blocked by Acquire is released. In other words, there is a MUTEX, only one thread can get the processor time between the acquire call and the release call. The execution code between the acquire call and the release call itself is a critical area. (Windows terminology is a bit confused because it makes Mutex itself a Critical Section (critical area). Although "MuteXT" is actually a process range MUTEX, but calling their thread MUTEX and process MUTX will be better.) Mutex It is used to protect data and prevent the actual conditions. Depending on the definition, when the result of the multi-thread pair data processing is determined by the thread, a active condition is generated. When two or more thread competition uses the same data, the actual conditions appear. Because the thread may be interrupted at any point in time, the data being processed may be destroyed or missed. As a result, the modification of the data must be carefully protected by the critical area when reading a modification operation or sometime. In object-oriented programming, this usually means that you store a MUTEX in a class as a member variable, when you use it when you access the data. Experienced multithreaded programmers may already beaten in the top two paragraphs, but the purpose of that paragraph is to provide a warm-up, because now we have to connect multi-threaded programming and volatile. We do this by taught the intersection of C 's type world and thread semantic world. * In addition to any thread, any thread can be interrupted at any time, there is no control, so the result is Volatile that is accessed by multiple threads. This also maintains the original intent of Volatile - prevents the compiler unclear cache the value used by multiple threads. * Define a MUTEX within the critical area, only one thread can be accessed. As a result, in a critical area, the execution code has a single-threaded environment. The variables used cannot be volatile - you can remove the Volatile identifier. In short, the data shared by multiple threads is volatile outside the critical area. It is non-volatile in the critical area. You can enter a critical area by locking a MUTEX. You remove the Volatile identifier by using a const_cast. If you put these two operations together, we have established a connection between the C type system and the application's thread. We can let the compiler to check our instance conditions for us.

LockingPtr We need a tool to focus on a MUTEX acquisition operation and a const_cast. Let's develop the LockingPTR template class, you can initialize this template class with a Volatile object OBJ and a MUTEX object MTX. During this template class, a LockingPtr keeps MTX always occupied. At the same time, LockingPtr provides access to OBJ to remove Volatile. This access is to provide with smart pointer, available via Operator-> and Operator *. Perform const_castr in LockingPTR, this conversion semantic is valid because LockingPtr keeps MUTEX in the survival period is occupied. First let's define the skeleton of the Mutex class used by LockingPtr:

Class Mutex {public: void acquire (); void release (); ...};

In order to use LockingPtr, you have to use the data structure and basic functions used in your operating system to implement MUTEX. LockingPTR uses controlled variables as templates. For example, if you want to manage a Widget, you use a LockingPtr so you can initialize it with a variable of Volatile Widget. The definition of LockingPtr is very simple. LockingPTR implements a relatively simple Smart Pointer. It is the purpose of just set a const_cast and a critical area. Template Class LockingPtr {public: // Construction / Destructure Function LockingPtr (Volatile T & Obj, Mutex & MTX): POBJ_ (CONST_CAST (& Obj)), PMTX_ (& MTX) {mtx.lock (); } ~ LockingPtr () {PMTX_-> unlock (); // Simulation Pointer T & OPERATOR * () {Return * Pobj_;} T * Operator -> () {Return POBJ_; Mutex * PMTX_; LockingPtr (const Lockingptr &); LockingPtr & Operator = (const Lockingptr &);

Although simple, LockingPtr is very helpful to write the correct multi-threaded code. You should define the objects shared by several threads as Volatile and cannot use const_cast - should always use the LockingPTR auto object. We use an example to explain: Suppose you have two threads sharing a vector object

Class syncbuf {public: void thread1 (); void thread2 (); private: typef vector buft; volatile buft buffer_; mutex mtx_; // Control to Buffer_ access};

In a thread function, you simply use a LockingPtr to get controlled access to the buffer_ member variable:

Void syncbuf :: thread1 () {LockingPtr lpbuf (buffer_, mtx_); buft :: item i = lpbuf-> begin (); for (; i! = lpbuf-> end (); i) { ... use * i ...}}

These code is very easy to write very easy to understand - any time you need to use buffer_, you must create a LockingPtr pointing it. Once you do this, you can use all interfaces of VECOTR. Very good thing is that if you make a mistake, the compiler will point out:

Void syncbuf :: thread2 () {// error, you can't call Begin () BUFT :: Iterator i = Buffer_.begin (); // Error! You can't call end () for (; i! = Lpbuf-> end (); i) {... use * i ...}}

You can't call any functions of Buffer_ unless you either use a const_cast or use LockingPtr. The difference is that LockingPTR provides an ordered way to use const_cast to volatile variables. LockingPTR is very expressive. If you only need to call a function, you can create an unknown temporary LockingPtr object and use it directly: unsigned int syncbuf :: size () {return lockingptr (buffer_, mtx _) -> size ();}

Back to the Basic Type We have seen how good the Volatile protection object is not accessed uncontrolled, and it has also seen how LockingPTR provides a simple and efficient way to write the thread security code. Let us return to the basic type, those with different types of Volatile behavior and user custom types, we consider an example, multiple threads share a variable of INT.

Class count {public: ... void increment () { CTR_;} void decrement () {--ctr_;} private: int CTR_;

If Increment and Decrement are called by different threads, the above code snippet is problematic. First, CTR_ must be Volatile, secondly, even if it is like CTR_, it is actually a three-step operation. Memory itself has no arithmetic ability, when incrementing a variable, the processor: * reads that variable to the register * Add value in the register * write the result back memory

This three-step operation is called RMW (Read-ModifyWrite Read - Change - Write). When performing an "change" operation of an RMW operation, most processors will release the memory bus in order to access the memory. If another processor performs an RMW operation to the same variable, we have a statement; the second write operation covers the first result. You can also avoid this situation with LockingPtr:

Class counter {public: ... void increment () { * lockingptr (CTR_, MTX_);} void Decrement () {- * LOCKINGPTR (CTR_, MTX_);} private: Volatile Int CTR_; MUTEX MTX_;

The code is now correct, but the code quality is compared to the code of SyncBuf, and there is a lot of words. why? Because in counters, if you are incorrectly accessing CTR_ (no first to lock it first) The compiler will not warn you. If CTR_ is Volatile, CTR_ can also be compiled, but the resulting code is obviously wrong. The compiler is no longer your helper, just paying attention to your own attention to avoid such acts. Then what should you do? Simply put the basic data you use as a higher level, for those structures. It's ridiculous, although the use of Volatile is used in the internal type of type, it is actually doing this is not a good idea!

So far, we already have classes containing Volatile data members. Now let's consider designing a class part of a larger object, these classes are also shared by multi-threaded sharing. There is a great help with the Volatile member function here. When designing your class, you only add a VoALTILE logo for member functions of thread secure. You must assume that the external code will call the volatile function at any time with any code. Don't forget: volatile is equal to freely for multi-threaded code without using critical regions, non-Volatile is equal to a single-threaded environment or in a critical area. For example, you define a widget class to implement two changes in a function - a thread is safe and a fast, no protection. Class widget {public: void Operation () volatile; void Operation (); ... private: mutex mtx_;};

Note that overload is used. Now Widget users can call Operation with the same syntax, regardless of the Operation of the thread security call Volatile object or to get the speed calling the regular object. However, users must carefully define the Widget objects shared by multi-threaded to volatile. When a Volatile member function is implemented, the first operation is usually to lock with a LockingPtr to this. The rest of the work can be handed over to the corresponding function of nonivata:

Void widget :: Operation () Volatile {LockingPtr lpthis (* this, mtx_); lpthis-> Operation (); // call non-volatile function}

Summary When writing multithreaded programs, you can get benefits with Volatile. You must abide by the following rules: * Define all shared objects to volatile. * Do not use the Volatile * when the basic type can be shared when the definition can be shared, using the Volatile member function to represent thread security.

If you do this, and if you use the simple return component LockingPtr, you can write out the thread secure code without having to consider the actual conditions, because the compiler can pay attention to you, will take the initiative to take the initiative to tell you wrong. local. Several people I have participated in a good effect using Volatile and LockingPtr. The code is clear and easy to understand. I remember to meet a few dead locks, but I am willing to encounter a deadlock or not, because the dead lock is more prone to more. In fact, no problem is about actual conditions.

Acknowledgment very thanked James Kanze and Sorin Jianu, they gave me a very helpful insight.

Supplement: Volatile is actually abused? I received many feedback on the "return : volatile-multi-line program" for me in February column articles. As usual, the praises I have received come from private letters, but complaining that they are sent in the USENET newsgroup Comp.lang.c . Moderated and Comp.Programming. Then the debate is fierce, if you are interested in this topic, you can go see. The post is named "Volatile, Was Memory Visibility Between Threads" I learned a lot from that post. One thing is that the Widget example starting in the text is wrong. For a long time, some system (such as POSIX compatibility system) does not need a Volatile identifier, and some other systems have been added, and the programs are still incorrect. The most important question is that volatile is depends on the Mutexes facilities similar to POSIX, and some multi-processor systems are not enough - you must use a memory barrier. Another more philosophical problem is that strict saying, converting Volatile from variables to be illegal, even if you have added Volatile flags for the correctness of Volatile. As Anthony Williams pointed out, a system may have enough reasons to store Volatile data in a place different from non-Volatile data, such address conversion behavior errors. There is another criticism that the correctness of Volatile. Although it solves the actual conditions in the low layer, it cannot correctly detect higher-level, logical actual conditions. For example, you have an MT_VECTOR class template to simulate a std :: vector, but have the correct synchronization member function. Volatile Mt_vector vec: if (! vec.empty ()) {vec.pop_back ();} The original intent is to remove a vector's last element, if any. The above code runs very well in a single-threaded environment, but if you use MT_Vector in multi-threaded program, the code may throw accidents, even if Empty and Pop_back have been properly synchronized. So the low-level data (VEC) consistency is kept correct, but higher levels of operation is wrong. After all discussions, no matter what, I still insist on recommending Voaltile is a useful tool to detect active conditions on systems similar to POSIX's Mutexes. But if you work under multiprocessor system, you may first read your text. You know what to do. Finally, Kenneth Chiu refers to a very interesting article in http://theory.stanford.edu/~freunds/race.ps. Guess what is the topic of article? "Type-based Race Detection for Java" article describes, adds very little things in the Java type system, plus programmers, can detect active conditions at compile time. (This supplement is published in the second month of the post-published column) Andrei Alexandrescu is a doctoral student at Washington University, Seattle, is also a Book of Modern C Design. You can contact him via www.moderncppdesign.com. Andrei is also a C seminar (). A superior lecturer.

Author Blog:

http://blog.9cbs.net/merced1976/

related articles

转载请注明原文地址:https://www.9cbs.com/read-111760.html

New Post(0)