Volatile's good helper writing multithreaded programs

xiaoxiao2021-04-01  243

Http: //dev.9 Press .NET / DEVELOP / Article / 83/83923.

It's not that I deliberately want to show your mood, but in this column, we will discuss this topic of multithreading programming. As mentioned in the previous period, the program writes an exception-safe is very difficult, but it is simply a play with a multi-threader program. Multi-threaded procedures are difficult to write, difficult to verify, difficult to debug, difficult to maintain, which is often bitter. Incorrect multi-threading programs may be able to run for many years, do not have a mistake until they meet certain critical conditions, only unexpected strange mistakes. Needless to say, programmers who write multithreaded programs need to use all possible helpments. This column will focus on discussing competitive conditions - this is usually the root of various troubles in multi-threaded programs - deeply understanding it and provides some tools to prevent competition. Amazing is that we will make the compiler to help you do these things. Just just an opaque keyword. Although C and C standards are obvious "keep silent" for threads, they do a privilege for multi-threaded privileges in the form of Volatile keywords. Just like a more familiar const, Volatile is a Type Modifier. It is designed to modify variables that are accessed and modified by different threads. If there is no volatile, it will basically lead to such results: Either you cannot write multithreaded programs, or the compiler will lose a lot of optimization opportunities. Let's explain one by one. Consider the following code: Class gadget {public: void wait () {while (! Flag_) {sleep (1000); // sleeps for 1000 milliseconds}} void wakeup () {flag_ = true;} ... private : bool flag_;}; The purpose of gadget :: wait in the above code is to check the FLAG_ member variable every one second. When Flag_ is set to True by another thread, the function returns. At least this is the intent of the executive author, however, this WAIT function is wrong. Suppose the compiler finds that Sleep (1000) is called an external library function, which does not change the member variable flag_, then the compiler can determine that it can put the FLAG_ cocked in the register, and you can access the register to replace accesses slower Memory on the motherboard. This is a good optimization for single-threaded code, but in this case, it destroys the correctness of the program: When you call a gadget's WAIT function, even if another thread calls WAKEUP, WAIT will still be loop. This is because the change of FLAG_ is not reflected in the register of the cache. The optimization of the compiler is not awkward ... optimistic. In most cases, it is a very valuable optimization method in the register, and it is a pity. C and C provide you with an opportunity to disable this cache optimization. If you declare that the variable is using the volatile modifier, the compiler does not cause this variable to cushically in the register - each access will go to the actual location of the variable in memory. This way you have to make changes to the gadget's wait / Wakeup is to add the correct modifications to FLAG_: Class gadget {public: ... as Above ... private: volatile bool flag_;}; Most of the principle of Volatile and The interpretation of the usage will be here, and it is recommended that you use the volatile to modify the native type variable used in multiple threads. However, you can do more things with volatile because it is part of a magical C type system.

Try the Volatile to customize type volatile modifications not only for native types, but also for custom types. At this time, the volatile modification is similar to const (you can also use const and volatile for a type.). Unlike const, Volatile's role is different from the native type and custom type. That is to say, when the native type has Volatile modification, it still supports their various operations (plus, multiplied, assignment, etc.), but for Class, it is not the case. For example, you can assign a non-Volatile's intimate value to an int, but you can't assign a non-Volatile object to a Volatile object. Let us give an example to illustrate how the custom type Volatile works. Code: Class gadget {public: void foo () volatile; void bar (); ... private: string name_; int 2_;}; gadget regulagadget; volatile gadget volatilegadget; if you think volatile is not What role, then you have to be shocked. Volatilegadget.foo (); // ok, volatile fun called for // volatile Object regulagadget.foo (); // ok, volatile fun caled for // Non-Volatile Object volatilegadget.bar (); // error! Non- Volatile Function Called for // Volatile Object! The conversion from the type without Volatile modified to the corresponding Volatile type is usually usually. However, just like const, you can't turn over the Volatile type to a nonvatile type. You must use type conversion operators: gadget & ref = const_cast (volatilegadget); ref.bar (); // OK A class with volatile modified class only allows access to a subset of its interface, this subset of classes Implementors are controlled. Users can only access all of this type of interface only with const_cast. Moreover, like Const, the class Volatile property is passed to its member (for example, volatilegadget.name_ and volatilegadget.state_ is also a volatile variable). The simplest synchronization mechanism is also MUTEX (mutually exclusive object) in the multi-threaded program. A MUTEX only provides two basic operations: acquire and release. Once a thread calls acquire, other threads will be blocked when Acquire is called. When this thread calls Release, it has just blocked in the thread in the acquire, there will be only one waken. In other words, for a given MUTEX, only one thread can get the processor time between acquire and release calls. The code executed between acquire and release calls called critical section. (WINDOWS may cause a little confusion, because Windows calls Mutex itself called critical regions, while Windows Mutex actually refers to Mutex between processes. If they are called thread MUTEX and process Mutex may be better.

Mutex is used to avoid competitive conditions in data. According to the definition, the so-called competitive condition is such a situation: the role of multiple threads to depends on the scheduling order of the thread. Competitive conditions will occur when the two threads are competing to access the same data. Because a thread can interrupt other threads at any time, the data may be destroyed or interpreted. Therefore, the modification operation of the data must be protected by critical regions in critical regions. In object-oriented programming, this usually means you save a MUTEX in a member variable, then use this MUTEX when you access this class. Multi-threaded programming masters have seen the above two paragraphs, may already beaten, but their purpose is to provide an preparing exercise, we have to contact Volatile. We will make a comparison of the type of C and threads. In a critical area, any thread breaks other threads at any time; this is not controlled, so variables accessed by multiple threads are easily changed. This is consistent with the origin of Volatile, so you need to use Volatile to prevent the compiler from unintentionally cache such variables. In a critical area defined by a Mutex, only one thread can enter. Therefore, the code executed in the critical regions has the same semantics as the single-threaded program. The controlled variable will not be changed again - you can remove the volatile modification. In short, the data shared between the thread is outside the critical area, and it is not within the critical area. You entered a critical zone by locking a MUTEX, then you remove a type of volatile modification, if we can successfully put these two operations together, then we are in C type systems and applications. The thread semantics establishes contact. This way we can let the compiler to help us detect competitive conditions. LockingPtr We need a tool to make Mutex acquisition and const_cast. Let's design a LockingPtr class, you need to use a volatile object OBJ and a MUTEX object MTX to initialize it. In the life-Lodging period of the LockingPtr, it guarantees that MTX is being acquired, and also provides access to OBJ-removing Volatile modified OBJ. Access to OBJ is similar to Smart Pointer, which is performed by Operator-> and Operator *. Const_cast is conducted within the LockingPtr. This transformation is correct in semantics because LockingPtr has Mutex in its survival. First, let's define the Mutex classes for work with LockingPtr: Code: Class Mutex {public: void acquire (); void release (); ...}; To use LockingPtr, you need to use the data structure provided by the operating system And the underlying function to achieve MUTEX. LockingPtr is a template that uses the type of control variable as a template parameter. For example, if you want to control a Widget, you have to write LockingPtr . LockingPtr's definition is simple, it just implements a simple smart Pointer. The focus of its attention is only to put Const_cast and critical regions.

Code: template class LockingPtr {public: // Constructors / destructors LockingPtr (volatile T & obj, Mutex & mtx): pObj_ (const_cast (& obj)), pMtx _ (& mtx) {mtx.Lock (); } ~ LockingPtr () {PMTX_-> unlock ();} // Pointer Behavior T & Operator * () {return * pobj_;} t * operator -> () {Return Pobj_;} private: t * pobj_; mutex * pmtx_ LockingPtr (const Lockingptr &); LockingPtr & Operator = (const LockingPtr &);}; Although it is very simple, LockingPtr is very useful for writing the correct multi-threaded code. You should declare the objects between the threads as Volatile, but never use const_cast - you should always use LockingPtr's Automatic Objects. Let us explain. For example, you have two threads that need to share a vector object: Class SyncBuf {public: void thread1 (); void thread2 (); private: typef vector buft; volatile buft buffer_; mutex mtx_; / / Controls Access to Buffer_}; In a thread function, you only need to use a LockingPtr object to get controlled access to the buffer_ member variable: code: void syncbuf :: thread1 () {LockingPtr < BUFT> LPBUF (Buffer_, MTX_); BUFT :: Iterator i = lpbuf-> begin (); for (; i! = Lpbuf-> end (); i) {... us * i ...} } This code is easy to write, it is also easy to understand - whenever you need to use Buffer_, you must create a LockingPtr to point to it. When you do this, you can visit the entire interface of the Vector. The advantage of this method is that if you make mistakes, the compiler will point 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 can't access Buffer_ Any function, unless you have const_cast, or use LockingPtr. The difference between the two is that LockingPTR provides a rule-to-regular way to const_cast-, a volatile variable. LockingPTR has a very good expression.

If you only need to call a function, you can create an unknown temporary LockingPtr object, then use it directly: Code: unsigned int syncbuf :: size () {Return LockingPtr (buffer_, mtx _) -> size (); } Back to the native type We have seen how excellent access to the protective object from uncontrolled access, and see how LockingPtr provides a simple and effective way to write thread secure code. Now let's go back to the native type, Volatile is different from them. Let us consider a plurality of threads share an example of an int variable. Code: Class Counter {public: ... void increment () { CTR_;} void Decrement () {--ctr_;} private: int Ctr_;}; if increment and decrement are called in different threads There is a bug in the code snippet above. First, CTR_ must be Volatile. Second, even one seems to be an atomic operation, such as CTR_, actually divided into three phases. Memory itself is no operational function. When an incremental operation is performed on a variable, the processor will add the variable read register to the value of the register 1 to write the result back to memory. This three step is called RMW (Read-modify -Write). In a modify phase of an RMW operation, most processors will release the memory bus to enable other processors to access memory. If another processor is also an RMW operation on the same variable at this time, we have encountered a competitive condition: the second write will overwrite the first value. To prevent such things, you have to use LockingPtr: Code: Class Counter {public: ... void increment () { * LockingPtr (CTR_, MTX_);} void Decrement () { * LOCKINGPTR (CTR_, MTX_);} private: Volatile Int Ctr_; Mutex MTX_;}; now this code is correct, but compared with SyncBuf, this code is worse. why? Because for counters, the compiler does not generate warnings when you are incorrectly accessing CTR_ (without locking it). Although CTR_ is Volatile, the compiler can also compile CTR_, although the resulting code is absolutely incorrect. The compiler is no longer your ally, you only have you pay attention to the competitive conditions. So what should you do? Very simple, you can use a high-level structure to pack native type data, then use Volatile to the structure. This is a little contradictory, directly use volatile modified native types is a bad usage, even though this is the usual use of Volatile! The Volatile member function is now, we discussed classes with Volatile data members; now let us consider designing such a class, which will be part of a larger object and shared between threads. Here, the member function of Volatile can help. When designing class, you only add volatile modifications to those who thread security. You must assume that the code outside will call the Volatile member function anytime at any time.

Don't forget: Volatile is equivalent to free multi-thread code, and there is no critical region; non-Volatile is equivalent to a single-threaded environment or within the critical area. For example, you define a Widget class that implements the same operation with two ways - a method of thread security and a fast unprotected approach. Code: Class Widget {public: void Operation () Volatile; void Operation (); ... private: mutex mtx_;}; Note Here the overloading. Now Widget's users can call Operation in a consistent syntax, and thread security can be obtained for the Volatile object, and the speed can be obtained for normal objects. Users must pay attention to define the shared Widget object as Volatile. When implementing the Volatile member function, the first operation is usually to lock this with LockingPtr, then the rest can be handed over to the same name function for non-Volatile: Code: void widget :: Operation () Volatile {LockingPtr LPTHIS (* this, MTX_); lpthis-> Operation (); // invokes the non-volatile function} Skumber When writing to the thread program, using Volatile will be very beneficial to you. You must adhere to the following rules: State all shared objects to volatile Do not use the volatile to use the volatile to define a shared class directly, use the Volatile member function to represent its thread security. If you do this, and use simple universal component LockingPtr, you can write out thread secure code, and greatly reduce the concerns of competitive conditions, because the compiler will worry about you, and diligently pointed out Where is wrong. In several projects I have participated, using Volatile and LockingPtr have a great effect. The code is very neat, it is easy to understand. I remember some deadlocks, but I would rather deal with the deadlock relative to the competition conditions, because they are more remarkable. Those projects actually did not encounter problems about competitive conditions. Acknowledgments Thank you very much, James Kanze and Sorin Jianu provide very insightful comments. Acknowledgments Thank you very much, James Kanze and Sorin Jianu provide very insightful comments. Attachment: Abuse of Volatile Essence? [2] After the previous column "Generic : Volatile - Multithreaded Programmer's Best Friend" published a lot of feedback. It is like it is destined, most of the praises are private letters, and they have been issued to the USENET newsgroup comp.lang.c . Moderated and Comp.Programming.Threads. Then there was a long and fierce discussion. If you are interested in this topic, you can take a look at this discussion, 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 the Widget at the beginning of the article is not too cut. Long-term short saying, in many systems (such as POSIX-compatible systems), volatile modifications are unnecessary, and in other systems, even if Volatile is added, the program is still incorrect.

About Volatile Correctness, the most important question is that it relies on Mutex similar to POSIX. If you are in multiprocessor systems, it is not enough - you must use Memory Barriers. Another more philosophical problem is: Strictly transform the Volatile property of the variable through type conversion, even if the Volatile property is yourself for Volatile Correctness. As Anthony Williams pointed out, you can imagine that a system may put the Volatile data in a storage area different from non-Volatile data, in which case the address transformation has uncertain behavior. Another criticism is that Although Volatile Correctness can solve competitive conditions at a lower level, it is not possible to correctly detect high-level, logical competitive conditions. For example, you have a MT_Vector template class, used to simulate the std :: Vector, and the member function is synchronized by the correct thread. Consider this code: volatile mt_vector vec; ... if (! Vec.empty ()) {vec.pop_back ();} This code is to delete the last element in the Vector if it exists. In a single-threaded environment, he works well. However, if you use it in a multi-threaded program, this code is still possible to throw an exception, although Empty and POP_BACK have the correct thread synchronization behavior. Although the consistency of the underlying data (VEC) is guaranteed, the results of the high-level operation are still uncertain. In any case, after the debate, I still maintain my suggestion, on the system of POSIX's Mutex, Volatile Correctness, a valuable tool for detecting competitive conditions. But if you are on a multiprocessor system that supports memory access, you first need to read your compiler's documentation carefully. You must know each other. Finally, Kenneth Chiu mentioned a very interesting article http://theory.stanford.edu/~freunds/race.ps, guess what is the topic? "Type-based Race Detection for Java". This article tells how to make a little complement to Java's type system, so that compilers and programmers have detected competitive conditions when compiling. Author Blog:

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

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

New Post(0)