Introduction to multithreading and thread synchronization in Windows
Multi-task is the ability of an operating system to run multiple programs at the same time. Basically, the operating system uses a hardware clock to allocate "time slices" for each process running simultaneously ". If the time slice is small enough and the machine is not overloaded by too many programs, it seems to the user that all these programs are in the same test run.
Multithreading is the ability to implement multiple tasks within a program. Programs can separate themselves into separate execution "Threads", which seem to be running at the same time [1]. [G1] multithreading is widely used. The most common practice is to use an auxiliary thread in a program that requires a large amount of computing to complete computation. the user interface thread responds to user operations.
Communication between different threads in multiple threads is usually implemented using shared data objects. Access conflicts may occur whether global variables or pointer parameters of Thread process functions are used for communication [2]. [G2] The method to solve this problem is thread synchronization.
Windows provides multiple methods to coordinate and synchronize threads, including critical section, event object, and mutex. These methods have their own characteristics and applicable occasions. Let's take a look at the method of using the critical section for thread synchronization described in the classic "windows program design" [3].
First, you need to define a global critical zone object so that it can be accessed in different threads. Example: critical_section Cs;
Then initialize the critical area object in a thread:
Initializecriticalsection (& CS );
In this way, a critical zone object named CS is created. In this case, the thread can enter the critical section through the following call:
Entercriticalsection (& CS );
At this time, the thread is considered as "having" an object in the critical section. No two threads can have a critical area object at the same time. Therefore, if a thread enters the critical section, the next thread that calls entercriticalsection using the same critical area object will be suspended in function calls. The function returns only when the first thread leaves the critical section through the following call:
Leavecriticalsection (& CS );
At this time, the suspended thread in the call of entercriticalsection has a critical section, and its function call also returns, allowing the thread to continue running.
When the critical section is no longer needed, you can call:
Deletecriticalsection (& CS );
Delete it. [G3]
After entering the critical section, the thread can exclusively access resources without worrying about interference from other threads. When different threads share different data, multiple critical sections can also be used.
The use of event objects and mutex is different from that in the critical section, but the process and steps are similar. However, you only need to call the waitforsingleobject function instead of the entercriticalsection function to block the thread, wait for other threads to release resources after execution.
It can be seen that thread synchronization is a complex and error-prone task. It ensures that each thread does not conflict with each other during data access and update, and also prevents deadlocks. In MFC, in order to reduce the complexity of thread synchronization and reduce the workload, the following classes are provided: ccriticalsection, cevent, and cmutex, which encapsulate the related thread synchronization functions in Windows APIs, convenience for programmers to use [4]. [G4] But the appearance of these classes does not change the basic process and steps of thread synchronization programming. In the specific use process, you still need to be careful when using it.
A simple method for implementing Thread Synchronization
Can we improve the thread synchronization method provided by windows and provide a simple and convenient implementation method? This method should not require tedious steps, but meet our requirements, at the same time, it is flexible enough.
First, let's review several thread synchronization mechanisms provided by windows. It can be found that a string can be used as the identifier of the thread synchronization object in the event object, mutex and semaphore usage. When a thread synchronization object with the name is created, if an object with the same name already exists, a handle of an existing object will be returned. You can call getlasterror to obtain the error_already_exists value to check whether a handle value of an existing object is returned.
Take the mutex as an example:
// Create the first mutex
Handle mutex1;
Mutex1 = createmutex (null, true, "mutex ");
If (error_already_exists = getlasterror () // There Is A mutex with the same name
{
Printf ("mutex exist! /N ");
}
Else // No mutex with the same name is found
{
Printf ("create mutex! /N ");
}
// Create the second mutex
Handle mutex2;
Mutex2 = createmutex (null, true, "mutex ");
If (error_already_exists = getlasterror () // There Is A mutex with the same name
{
Printf ("mutex exist! /N ");
}
Else // No mutex with the same name is found
{
Printf ("create mutex! /N ");
}
Compile and run the above Code in VC ++ 6.0. The result is as follows:
Create mutex!
Mutex exist!
We can see that when "mutex" is used as the name to create mutex for multiple times, we can check the returned value of getlasterror to determine whether the mutex is created for this name for the first time. The following two functions can be further developed:
Handle lock (char * name)
{
Handle mutex;
// Try to open an exist mutex firstly.
Mutex = openmutex (mutex_all_access, false, name );
If (null = mutex) // If the mutex does not exist, create it with the certain name.
{
Mutex = createmutex (null, true, name );
}
Else // If the mutex already exist, wait for other thread release it.
{
Waitforsingleobject (mutex, infinite );
}
Return mutex;
}
Bool unlock (handle mutex)
{
If (0 = releasemutex (mutex) // failed to release mutex
{
Return false;
}
Else // successed in release mutex
{
Closehandle (mutex );
Mutex = NULL;
Return true;
}
}
Only the following calls are required during use:
Handle mutex = Lock ("mutexlockname ");
......
Unlock (mutex );
The name of the mutex when the lock is called. If there is no mutex with the same name, call createmutex to create a mutex with the name variable value; if a mutex with the same name already exists, the openmutex function returns a handle to the existing mutex. At this time, the current thread is blocked by calling waitforsingleobject, and the system continues to run after the mutex with the same name is released.
When unlock is called, a mutex handle is passed in. The releasemutex is used to release the thread's ownership of the mutex, And the handle is closed to prevent resource leakage.
With the above two functions, we can lock the same resource by using the same name to implement thread synchronization. For example, an object exists in different threads and is defined as follows:
Cobject object;
When we access and change this object, we only need to make the following calls:
Handle mutex = Lock ("object ");
Object. dosomething ();
Unlock (mutex );
It is very convenient to complete the thread synchronization for this object.
Through the lock and unlock functions, we hide some specific details when using mutex, reducing complexity, but there are still many shortcomings in this method, the following describes how to improve these problems step by step.
During use, a name must be passed into the lock function as a parameter, and the name must be consistent when locking the same data object to ensure the normal operation of the lock function, this creates unnecessary trouble for users. If the following statement exists:
Cobject * pobject1 = new cobject;
Cobject * pobject2 = pobject1;
Cobject * pobject3 = pobject2;
It can be seen that pobject1, pobject2, and pobject3 actually point to the same object. At this time, if you want to perform thread synchronization, you must use the same name when locking the three pointers. The alias problem occurs.
In C ++, variable names can be the same in different scopes. Therefore, the same pobject may point to different objects in different scopes, that is, the same name.
For aliases and the same names, how can we identify and name the same object in a uniform manner? In C ++, each class has a this pointer to point to its own storage address. In a program, different objects have different storage addresses in the memory. This address identifies different objects like the ID card number, which is also the unique name we want to get for different objects. The lock function can be transformed:
Handle lock (void * pointer)
{
Char name [128];
ITOA (DWORD) pointer, name, 16 );
// The following is the same as the preceding
Handle mutex;
// Try to open an exist mutex firstly.
Mutex = openmutex (mutex_all_access, false, name );
......
}
In this way, you do not need to manually name each object during the call. You only need to pass in the object address:
Handle mutex = Lock (pobject );
......
Unlock (mutex );
This saves the trouble of naming.
The main problem has been solved step by step, but the program looks ugly. Lock and unlock must be used as global functions, which violates the object-oriented design principles, in addition, every time a pair of calls are required, it is inevitable that a careless programmer calls lock [G5] But forgets to unlock, so the program may enter the deadlock [ms6].
To make it easier to use, create a cmutexlock class, use lock and unlock as member functions, and then review the previous program, it is not difficult to find that the handle mutex parameter of unlock can also be moved into cmutexlock to become its member variable:
Class cmutexlock
{
Public:
Cmutexlock ();
Virtual ~ Cmutexlock ();
Bool lock (void * pointer );
Bool unlock ();
PRIVATE:
Handle m_mutex;
};
In this case, you only need to declare a cmutexlock object before calling the lock and unlock functions.
When lock and unlock become member functions of a class, the following question arises:
"Is it necessary to expose these two functions as public methods ?" Since the lock and unlock must be paired, isn't the constructor and destructor that exactly correspond to the class? Why not put one of them into the constructor and the other into the Destructor? When you raise these questions, you can also find the answer:
Class cmutexlock
{
Public:
Cmutexlock (void * pointer );
Virtual ~ Cmutexlock ();
PRIVATE:
Bool lock (void * pointer );
Bool unlock ();
Handle m_mutex;
};
Cmutexlock: cmutexlock (void * pointer)
: M_mutex (null)
{
This-> lock (pointer );
}
Cmutexlock ::~ Cmutexlock ()
{
This-> unlock ();
}
......
The call to cmutexlock is also simplified to a Definition Statement:
{
Cmutexlock lock (pobject );
......
}
When the lock is declared, the lock function is called for the input pobject of the constructor. When the lock is removed from the living space and destroyed, the unlock function is called in the destructor to release the mutex. Here, you can use {} to control the call of cmutexlock destructor, that is, release the lock on pobject. In C ++, {} can be used to control the scope of variables. Locks declared in {} will be destroyed when} is encountered, and the Destructor will be automatically called before destruction.
So far, we have completed a simple and convenient thread synchronization helper class.
Summary
In this article, the cmutexlock class is created step by step based on Windows multithreading mechanism and some basic features of C ++, thread Synchronization is simplified from the tedious and complex process to a one-to-one {} statement. Cmutexlock is easy to use. It can lock different data objects separately and reduce the occurrence of deadlocks. It works well in the actual program and shows the practicability of this method.
However, in the process of use, the mutex is inevitable due to its own characteristics and some disadvantages brought by the implementation methods in this article, for example, name conflicts between different processes, efficiency problems, constructor errors, and alias problems. These questions are not described in detail here. If you are interested, you can send a letter to discuss, mail address: deeplymove@tom.com.
References:
[1] p1119 windows programming (5th) Charles Petzold Peking University Press
[2] p263 visual c ++ 6.0 technology insider (version 5th) David J. kruglinski Scot Wingo George Shepherd hope Press
[3] p1147 windows programming (5th) Charles Petzold Peking University Press
[4] msdn
[G1] p1119 windows programming (5th) Charles Petzold Peking University Press
[G2] p263 visual c ++ 6.0 technology insider (version 5th) David J. kruglinski Scot Wingo George Shepherd hope Press
[G3] p1147 windows programming (5th) Charles Petzold Peking University Press
[G4] msdn
[G5]
[Ms6] refactoring