I introduced a convenient way to use mutex lock by closing the critical section object. All examples in this article are two threads reading and writing the same data, therefore, they must be mutually exclusive and cannot be accessed at the same time. In actual situations, there may be more complicated situations, that is, multiple threads access the same data, some of which are read and some are write. We know that problems may occur only when the read-write or write-write operations are performed simultaneously, while read-read operations can be performed simultaneously because they do not modify the data, therefore, it is necessary to encapsulate a convenient lock in C ++ that allows read-read concurrency, read-write, and write-write mutex. To implement this lock, it is very difficult to use the critical section. Instead, use the kernel object. Here I use mutex ).
The overall structure is similar to that in the previous article. It is to write a base class that encapsulates the lock, and then write a class that is used to call the ADD and unlock functions, locks and unlocks the lifecycle of the second category. There are two new problems involved here: locking and unlocking; one is adding/interpreting locks; the other is adding/decoding locks; second, in order to allow read-read concurrency, it is not enough to declare only one mutex here. Multiple mutex must be declared, and the number of mutex is equal to the number of concurrent read threads, this is because the API function we want to use is waitformultipleobjects.
The waitformultipleobjects function is used to wait for the object state to be set. The description in msdn is as follows:
Waits until one or all of the specified objects are in the signaled state or the time-out interval elapses.
This is a very useful function. We can use it to wait for a certain number of objects and allow you to set the timeout time. The value returned when waiting for success is different from that returned when the timeout time is reached. If the returned value is smaller than wait_abandoned, it indicates the waiting is successful. "Waiting for success" has different meanings for different types of kernel objects. For example, for a process or thread object, waiting for success indicates that the process or thread execution has ended. For mutex objects, this indicates that the object is not owned by any other thread. Once the thread waits for a successful execution, the current thread has the mutex, and other threads cannot have the mutex at the same time, directly call the releasemutex function to actively release the mutex.
Similar to waitformultipleobjects, there is also a function waitforsingleobject, which has simple functions and only applies to a single object, while waitformultipleobjects can wait for multiple objects at the same time and set whether to wait for all objects.
The instancelockbase class used in the previous article encapsulates a critical section object. Here we want to encapsulate a set of mutex handle. How many of them are there? It should be defined by a program that uses this class. For example, the dynamic array method can be used:
// Base class: Class rwlockbase // indicates read/write lock ...{ Handle * handles; Protected: Rwlockbase (INT handlecount)... {handles = new handle [handlecount];} ... }; // Subclass: Class myclass: Public rwlockbase ...{ Myclass (): rwlockbase (3 )...{} ... }; |
This is indeed a good method. By calling the base class constructor in the initialization section of the subclass constructor and passing the parameter, this dynamic array can be correctly initialized, but it looks not so good, the subclass must contain the word "rwlockbase" twice. Can it be inherited as long as instancelockbase? The answer is yes. You only need to use the C ++ template:
Template <int maxreadcount> Class rwlockbase ...{ Handle handles [maxreadcount]; ... }; |
The use of the template comes with such a benefit, because the template parameters can be determined during the compilation period, so you do not need to use dynamic arrays to allocate them directly on the stack. The use of templates leads to a new problem, that is, the type declaration of the Object Pointer passed by the corresponding lock class (rwlock) during construction, which is directly written as rwlock (rwlockbase * pobj) certainly not, because the template parameter must be specified and its value must be consistent with the value specified when rwlockbase is declared. Therefore, the client code must specify the template parameter value twice, unhappy! The solution is to convert rwlockbase into an interlayer class and declare a base class for it so that rwlock can receive base class pointers, and put the lock, unlock and other functions in the base class, declared as pure virtual functions, implementation written in the sandwich class:
Class _ rwlockbase ...{ Friend class rwlock; Protected: Virtual DWORD readlock (INT timeout) = 0; Virtual void readunlock (INT handleindex) = 0; Virtual DWORD writelock (INT timeout) = 0; Virtual void writeunlock () = 0; }; |
The template class rwlockbase inherits from _ rwlockbase and writes the following four functions:
Template <int maxreadcount = 3> // here, a default parameter is provided to minimize the amount of client code. Class rwlockbase: Public _ rwlockbase ...{ Handle handles [maxreadcount]; DWORD readlock (INT timeout) // Add a read lock. Wait until a mutex is returned. ...{ Return: waitformultipleobjects (maxreadcount, handles, false, timeout ); } Void readunlock (INT handleindex) // interpret the lock to release the obtained mutex ... DWORD writelock (INT timeout) // Add a write lock and wait until all mutex counts are exclusive to all other threads. ...{ Return: waitformultipleobjects (maxreadcount, handles, true, timeout ); } Void writeunlock () // unlock lock to release all mutex ...{ For (INT I = 0; I <maxreadcount; I ++) : Releasemutex (handles [I]); } Protected: Wlockbase () // constructor to initialize each mutex ..{ For (INT I = 0; I <maxreadcount; I ++) Handles [I] =: createmutex (0, false, 0 ); } ~ Rwlockbase () // destructor to destroy objects ...{ For (INT I = 0; I <maxreadcount; I ++) : Closehandle (handles [I]); } }; |
The corresponding lock classes will be slightly more complex:
Class rwlock ...{ Bool locksuccess; // whether to wait for a success because timeout may occur Int readlockhandleindex; // For read locks, you need to know which mutex is obtained _ Rwlockbase * _ pobj; // base class pointer of the target object Public: // Here, the second parameter is used to determine whether to apply the read lock or write lock. The third parameter is the time-out period. Rwlock (_ rwlockbase * pobj, bool readlock = true, int timeout = 3000) ...{ _ Pobj = pobj; Locksuccess = false; Readlockhandleindex =-1; If (null = _ pobj) Return;If (readlock) // read lock ...{ DWORD retval = _ pobj-> readlock (timeout ); If (retval <wait_abandoned) // If the returned value is smaller than wait_abandoned, the operation succeeds. ... {// Its value minus wait_object_0 is the array subscript Readlockhandleindex = retval-wait_object_0; Locksuccess = true; } } Else ...{ Word retval = _ pobj-> writelock (timeout ); If (retval <wait_abandoned) // when the lock is written, all mutex is obtained, and no subscript is saved. Locksuccess = true; } } ~ Rwlock () ...{ If (null = _ pobj) Return; If (readlockhandleindex>-1) _ Pobj-> readunlock (readlockhandleindex ); Else _ Pobj-> writeunlock (); } Bool islocksuccess () const... {return locksuccess ;} }; |
In this way, the read/write lock class is completed, and it is similar to instancelock in use:
1. The locked object inherits from rwlockbase <> class
2. When a read lock is required, declare an rwlock instance and point out that the read lock is to be added.
3. When a write lock is required, declare an rwlock instance and point out that the write lock is required.
I would like to say a few more words here. Although the use of pure virtual functions combined with the template class minimizes the amount of client code, it has some performance impact, because the virtual function is declared, there must be a four-byte vptr In the instance. when calling the virtual function, you need to find the vtable, with a small sacrifice in space and time. If you do not use a template class, there is no virtual function cost, but there is also a sacrifice: if you do not use a template class, you need to use a dynamic array. The dynamic array itself needs to be allocated on the heap when the program is running, this also takes time; the pointer to the dynamic array also occupies memory, so the space unlocking is the same, in terms of time, although it takes a little longer to dynamically allocate memory than to call a virtual function, initialization only takes once, which is also worthwhile in general. So what kind of data will be used in the end depends on the specific needs.
An experiment is also provided here. The lock class used here is similar to the previous one, simply inheriting from the rwlockbase class:
Class myclass2: Public rwlockbase <> ...{}; Myclass2 MC2; |
Let's look at two thread functions:
// Read thread DWORD callback readthreadproc (lpvoid PARAM) ...{ Int I = (INT) Param; Rwlock lock (& MC2); // Add a read lock If (lock. islocksuccess () // If the lock succeeds { Say ("read thread % d started", I); // to make the code shorter, assume that the say function has this capability. Sleep (1000 ); Say ("read thread % d ended", I ); } Else // lock timeout, the timeout information is displayed Say ("read thread % d timeout", I ); Return 0; } // Write thread DWORD callback writethreadproc (lpvoid PARAM) ...{ Int I = (INT) Param; Rwlock lock (& MC2, false); // Add a write lock. If (lock. islocksuccess ()) ...{ Say ("Write thread % d started", I ); Sleep (600 ); Say ("Write thread % d ended", I ); } Else Say ("Write thread % d timeout", I ); Return 0; } |
Main thread:
Int I; For (I = 0; I <5; I ++) : Createthread (0, 0, readthreadproc, (lpvoid) I, 0, 0 ); For (I = 0; I <5; I ++) : Createthread (0, 0, writethreadproc, (lpvoid) I, 0, 0 ); |
The program has a total of 10 threads and five reads and writes. When inheriting from the rwlockbase class, we use the default template parameters, so a maximum of three read threads are allowed at the same time. The program running result is as follows:
001 [15:07:28. 484] Read Thread 0 started 002 [15:07:28. 484] Read thread 1 started 003 [15:07:28. 484] Read thread 2 started 004 [15:07:29. 484] Read Thread 0 ended [15:07:29. 484] Read thread 3 started 006 [15:07:29. 484] Read thread 1 ended 007 [15:07:29. 484] Read thread 4 started 008 [15:07:29. 484] Read thread 2 ended 484 [15:07:30.] Read thread 3 ended 010 [15:07:30. 484] Read thread 4 ended 011 [15:07:30. 484] Write Thread 0 started 012 [15:07:31. 078] Write Thread 0 ended 013 [15:07:31. 078] Write thread 1 started 014 [15:07:31. 484] Write thread 2 timeout 015 [15:07:31. 484] Write thread 3 timeout 016 [15:07:31. 484] Write thread 4 timeout 017 [15:07:31. 687] Write thread 1 ended |
The three read threads in the first three rows obtain the read lock. After one second (Row 4-8), the three read threads are finished, and the remaining two read threads obtain the read lock, at this time, a mutex is not used, but because other threads request a write lock, the write lock is mutually exclusive with all other threads, so the write lock cannot be obtained. In another second (row 9-11), the two threads that obtained the read lock also ended, and the first write thread obtained the write lock. After 600 milliseconds (Row 12-13), the first write thread ends and the second write thread starts. After 400 milliseconds (line 14-16), the remaining three write threads timed out, and the second write thread ended.