Thread Synchronization in Windows

The operating mode of the Windows operating system is “dual-mode operation”: user mode and kernel mode.

User mode: The basic mode for running applications, prohibiting access to physical devices, and restricting access to memory areas. (the running mode of the application)

Kernel mode: The mode in which the operating system is running, not only does not restrict the accessed memory area, but also the accessed hardware devices are not restricted. (operating mode of the operating system)

In actual operation, Windows will not always be in user mode, but switch between user mode and kernel mode. For example, it is the operating system that creates threads, so it is unavoidable to switch to kernel mode during the process of creating threads.

User mode synchronization: fast but has certain limitations.

Kernel-mode synchronization: Provides more functions than user-mode synchronization; at the same time, timeouts can be specified to prevent deadlocks.

Synchronization based on user mode (CRITICAL_SECTION)

In this case, the CRITICAL_SECTION object will be created and used in the synchronization, which is a key to enter the critical section. Therefore, the key CRITICAL_SECTION needs to be obtained in order to enter the critical section. On the contrary, if you want to leave the critical area, you need to hand in this key. The following introduces the initialization and destruction related functions of the CS object.

#include

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

The lpCriticalSection Init function passes in the address value of the CRITICAL_SECTION object to be initialized, whereas the Delete function passes in the address value of the CRITICAL_SECTION object to be touched.

The following introduces the functions of obtaining and releasing CS objects, which can be simply understood as the functions of obtaining and releasing keys

#include

void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

Among them, lpCriticalSection is the address value of CS.

This part is similar to the mutex in Linux, and then we will directly write the sample program:

#include<iostream>
#include <windows.h>
#include<process.h> //thread library
using namespace std;

unsigned WINAPI read(void *arg);
unsigned WINAPI accu(void *arg);

int num,sum=0;
CRITICAL_SECTION cs;

int main()
{
HANDLE handles[100];
int i;
\t
InitializeCriticalSection( &cs);
for(i=0;i<10;i ++ ){
if(i%2==0)
handles[i]=(HANDLE)_beginthreadex(NULL,0,read,NULL,0,NULL); //create thread
else
handles[i]=(HANDLE)_beginthreadex(NULL,0,accu,NULL,0,NULL); //Create a summation thread
}
\t
WaitForMultipleObjects(10, handles, TRUE, INFINITE);
DeleteCriticalSection( &cs);
return 0;
}

unsigned WINAPI read(void *arg)
{
EnterCriticalSection( &cs);
cout<<"Input num:";
cin>>num;
LeaveCriticalSection( &cs);
return 0;
}

unsigned WINAPI accu(void *arg)
{
EnterCriticalSection( &cs);
sum + =num;
cout<<"sum = "<<sum<<endl;
LeaveCriticalSection( &cs);
return 0;
}

After the program runs, it is shown in the figure below:

based on the synchronization method in kernel mode

Typical kernel synchronization methods include synchronization based on kernel objects such as time, semaphore, and mutex, which will be introduced one by one below.

Synchronization based on Mutal Exclusion objects

The synchronization method based on the mutex object is similar to the synchronization method based on the CS object, so the mutex object can also be understood as a key. First, the function to create a mutex object is introduced.

#include

HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);

lpMutexAttributes pass security-related configuration information, use the default security settings can pass NULL.

If bInitialOwner is TRUE, the created mutex object belongs to the thread calling this function, and enters the non-signaled state at the same time, if it is FALSE, the created mutex object does not belong to any thread, and the state is signaled at this time.

lpName is used to name the mutex object. Creates an unnamed mutex object when NULL is passed in.

It can be seen from the above parameters that if the mutex object does not belong to any owner, it will enter the signaled state and use this feature for synchronization. In addition, the mutex belongs to the kernel object, so it is destroyed and destroyed by the following function.

BOOL CloseHandle(HANDLE hObject);

hObject is the handle to the kernel object to be destroyed

BOOL ReleaseMutex(HANDLE hMutex);

hMutex is the mutex object handle that needs to be released (de-owned)

Next, analyze the process of acquiring and releasing the mutex. When the mutex is acquired by a thread (when it is owned), it is in the non-signaled state, and when it is released (when it is not owned), it enters the signaled state. Therefore, you can use the WaitForSingleObject function to verify that the mutex has been allocated. The result of calling this function is as follows:

Enter the blocking state after calling: the mutex object has been acquired by other threads, and is now in the non-signaled state.

Return directly after calling: other threads occupy the mutex object, and are now in the signaled state.

The mutex automatically enters the non-signaled state when the WaitForSingleObject function returns.

So the actual operation to avoid deadlock is as follows:

WaitForSingleObject(hMutex, INFINITE);

// start of critical section

……………………………..

// end of critical section

ReleaseMutex(hMutex);

The ReleaseMutex function makes the mutex re-enter the signaled state, so it is equivalent to the exit of the critical section.

Next, a sample program for mutex synchronization in kernel mode is given as follows:

#include<iostream>
#include <windows.h>
#include <process.h>
using namespace std;

unsigned WINAPI read(void *arg);
unsigned WINAPI accu(void *arg);
unsigned int num;

int sum=0;

HANDLE hMutex;
int main()
{
HANDLE handles[100];
int i;
\t
hMutex=CreateMutex(NULL,FALSE,NULL);
for(i=0;i<10;i ++ )
{
if(i%2==0)
handles[i]=(HANDLE)_beginthreadex(NULL,0,read,NULL,0,NULL);
else
handles[i]=(HANDLE)_beginthreadex(NULL,0,accu,NULL,0,NULL);
}
\t
WaitForMultipleObjects(10, handles, TRUE, INFINITE);
CloseHandle(hMutex);
return 0;
}

unsigned WINAPI read(void *arg)
{
WaitForSingleObject(hMutex, INFINITE);
cout<<"Input num:";
cin>>num;
ReleaseMutex(hMutex);
return 0;
}

unsigned WINAPI accu(void *arg)
{
WaitForSingleObject(hMutex, INFINITE);
sum + =num;
cout<<"sum:"<<sum<<endl;
ReleaseMutex(hMutex);
return 0;
}

The code after the program runs is as follows:

Synchronization based on semaphore objects

The synchronization based on the semaphore object in Windows is similar to the semaphore in Linux. Both of them use the integer value named “semaphore value” to complete the synchronization, and the value cannot be less than 0.

The following introduces the object function of creating a semaphore, and the destruction also uses the CloseHandle function.

HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES lpMutexAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName);

lpMutexAttributes pass security-related configuration information, use the default security settings can pass NULL.

lInitialCount specifies the output hysteresis of the semaphore, which should be greater than 0 and less than lMaximumCount.

lMaximumCount The maximum value of the semaphore. When the value is changed to 1, the semaphore becomes a binary semaphore that can only represent 0 and 1.

lpName is used to name the mutex object. Creates an unnamed mutex object when NULL is passed in.

It can be synchronized by using the feature that the semaphore value enters the non-signaled state when it is 0, and enters the signaled state when it is greater than 0. When 0 is passed to the lInitialCount parameter, a semaphore object in the non-signaled state is created. When 3 is passed to lMaximumCount, the maximum value of the semaphore is 3, so the synchronization of 3 threads accessing the critical section at the same time can be realized. The function to release the semaphore object is introduced below.

BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);

hSemaphore passes the semaphore object that needs to be released

The release of IRleaseCount means the increase of the semaphore, and the increased value can be specified through this parameter. If it exceeds the maximum value, it will not increase and return FLASE.

lpPreviousCount is used to save the variable address of the value before modification, and NULL can be passed if it is not needed.

So the actual operation to avoid deadlock is as follows:

WaitForSingleObject(hSemaphore, INFINITE);

// start of critical section

……………………………..

// end of critical section

ReleaseSemaphore(hSemaphore, 1, NULL)

The following is a sample program based on semaphore synchronization and the final display results:

#include<iostream>
#include <windows.h>
#include <process.h>
using namespace std;
unsigned WINAPI read(void *arg);
unsigned WINAPI accu(void *arg);

static HANDLE demone;
static HANDLE semtwo;
static int num;

int main()
{
HANDLE hThread1, hThread2;
semone=CreateSemaphore(NULL,0,1,NULL); //Set semone to no-signaled state, let the thread main function wait (accu)
semtwo=CreateSemaphore(NULL,1,1,NULL); //Set semtwo to the signaled state, let the thread main function (read) run without waiting
\t
//Create thread
hThread1=(HANDLE)_beginthreadex(NULL,0,read,NULL,0,NULL);
hThread2=(HANDLE)_beginthreadex(NULL,0,accu,NULL,0,NULL);
\t
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
\t
CloseHandle(semone);
CloseHandle(semtwo);
return 0;
}

unsigned WINAPI read(void *arg)
{
int i;
for(i=0;i<5;i ++ )
{
cout<<"Input num:";
WaitForSingleObject(semtwo,INFINITE); //Run the rest when semtwo is signaled, let the value -1 become no-signaled state
cin>>num;
ReleaseSemaphore(semone,1,NULL); //Give semone + 1 to set it to the signaled state to let accu run
}
return 0;
}

unsigned WINAPI accu(void *arg)
{
int sum=0,i;
for(i=0;i<5;i ++ )
{
WaitForSingleObject(semone,INFINITE); //Run the remaining part when semone is signaled, and let its value -1 change to no-signaled state to continue waiting
sum + =num;
ReleaseSemaphore(semtwo,1,NULL);
}
cout<<"result:"<<sum<<endl;
return 0;
}

The result of running the sample program is as follows:

Synchronization based on event objects:

The event synchronization object is very different from the previous two synchronization methods. The difference is that when creating an object in this way, it can automatically run in the auto-reset mode in the non-signaled state and the opposite manual-reset mode. Choose one of them. The main feature of the event object is that it can create an object in manual-reset mode. First introduce the function to create the event object.

HANDLE CreateEvent(

LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName);

Returns the event object handle on success, or NULL on failure

lpMutexAttributes pass security-related configuration information, use the default security settings can pass NULL.

bManualReset creates an event object in manual-reset mode when TRUE is passed in, and creates an event object in auto-reset mode when FALSE is passed in.

When bInitialState is passed in TRUE, an event object in signaled state is created, otherwise, an event object in non-signaled state is created

lpName is used to name the mutex object. Creates an unnamed mutex object when NULL is passed in.

The following functions can be used to change the object state:

BOOL ResetEvent(HANDLE hEvent); //to the non-signaled

BOOL SetEvent(HANDLE hEvent); //to the signaled

The following example will introduce the specific usage of the event object, and the two threads in this example will wait for the input string at the same time.

#include<stdio.h>
#include <windows.h>
#include <process.h>
#define STR_LEN 100

unsigned WINAPI NumberofA(void *arg);
unsigned WINAPI Number of Others(void *arg);

static char str[STR_LEN];
static HANDLE hEvent;

int main()
{
HANDLE hthread1,hthread2;
hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
hthread1=(HANDLE)_beginthreadex(NULL,0,NumberofA,NULL,0,NULL);
hthread2=(HANDLE)_beginthreadex(NULL,0,Number of Others,NULL,0,NULL);
\t
fputs("Input string:",stdout);
fgets(str, STR_LEN, stdin);
SetEvent(hEvent); //Set the event object to the signaled termination state after reading the string.
\t
WaitForSingleObject(hthread1,INFINITE);
WaitForSingleObject(hthread2,INFINITE); //Wait for all of them to become signaled
ResetEvent(hEvent); //Set it to non-signaled
CloseHandle(hEvent);
return 0;
}

unsigned WINAPI NumberofA(void *arg)
{
int i,cnt=0;
WaitForSingleObject(hEvent, INFINITE);
for(i=0;str[i]!=0;i + + )
{
if(str[i]=='A')
cnt + + ;
}
printf("Num of A: %d\\
",cnt);
return 0;
}

unsigned WINAPI Number of Others(void *arg)
{
int i,cnt2=0;
WaitForSingleObject(hEvent, INFINITE);
for(i=0;str[i]!=0;i + + )
{
if(str[i]!='A')
cnt2++;
}
printf("Num of others: %d\\
",cnt2-1);
return 0;
}

The running results are as follows: