THREAD SYNCHRONIZATION IN C++
Threads:
a) One of the benefits of using multiple threads in an application is that each thread executes asynchronously.
b) For Windows applications, this allows time-consuming tasks to be performed in the background while the application window and controls remain responsive.
c) For server applications, multithreading provides the ability to handle each incoming request with a different thread. Otherwise, each new request would not get serviced until the previous request had been fully satisfied.
a) One of the benefits of using multiple threads in an application is that each thread executes asynchronously.
b) For Windows applications, this allows time-consuming tasks to be performed in the background while the application window and controls remain responsive.
c) For server applications, multithreading provides the ability to handle each incoming request with a different thread. Otherwise, each new request would not get serviced until the previous request had been fully satisfied.
Problems with Multithreading:
Multithreading improves the performance and utilization of CPU, but it also introduces various problems.
a) Deadlock
b) Race Condition
c) Starvation
Deadlock:
A deadlock occurs when two or more threads are blocked forever because they are each waiting for shared resources that the other threads hold. This creates a cycle of waiting, and none of the threads can proceed.
Race Condition:
A race condition occurs when two or more threads access shared resources at the same time, and at least one of them modifies the resource. Since the threads are competing to read and write the data, the final result depends on the order in which the threads execute, leading to unpredictable or incorrect results.
Starvation:
Starvation occurs when a thread is continuously unable to access shared resources because other threads keep getting priority, preventing it from executing and making progress.
Synchronizing Execution of Multiple Threads:
To avoid race conditions and deadlocks, it is necessary to synchronize access by multiple threads to shared resources. Synchronization is also necessary to ensure that interdependent code is executed in the proper sequence.
Synchronization Objects:
A synchronization object is an object whose handle can be specified in one of the wait functions to coordinate the execution of multiple threads.
Synchronization Primitives: Critical Section, Mutexes, Semaphores, Events
A) Critical Section:
The primary purpose of a critical section is to enforce mutual exclusion, meaning that when one thread is actively modifying shared data within the critical section, all other threads attempting to access the same critical section must wait until the first thread exits. This prevents inconsistent states that can arise when multiple threads try to modify shared resources concurrently.
CRITICAL_SECTION //critical section object
InitializeCriticalSection() //initialize the critical section
EnterCriticalSection() //request ownership of a critical section
LeaveCriticalSection() //release ownership of a critical section
DeleteCriticalSection() //release the system resources that are allocated
Example:
#include <Windows.h>
#include <iostream>
using namespace std;
#include <iostream>
using namespace std;
CRITICAL_SECTION csObj;
DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
EnterCriticalSection(&csObj);
DWORD tID = GetCurrentThreadId();
cout << "MyThreadFunction: CurrentThreadID = " << tID << endl;
for (int i = 0; i < 100; i++)
{
cout << "CurrentThreadID = " << tID << " : MyThreadFunction[" << i << "]" << endl;
}
LeaveCriticalSection(&csObj);
for (int i = 0; i < 100; i++)
{
cout << "CurrentThreadID = " << tID << " : MyThreadFunction[" << i << "]" << endl;
}
LeaveCriticalSection(&csObj);
return 0;
}
}
int main()
{
InitializeCriticalSection(&csObj);
DWORD dwThreadId[2];
LPVOID param;
HANDLE h[2];
LPVOID param;
HANDLE h[2];
h[0] = CreateThread(NULL, 0, MyThreadFunction, 0, 0, &dwThreadId[0]);
h[1] = CreateThread(NULL, 0, MyThreadFunction, 0, 0, &dwThreadId[1]);
WaitForMultipleObjects(2, h, 1, 1000);
h[1] = CreateThread(NULL, 0, MyThreadFunction, 0, 0, &dwThreadId[1]);
WaitForMultipleObjects(2, h, 1, 1000);
if (h[0])
CloseHandle(h[0]);
CloseHandle(h[0]);
if (h[1])
CloseHandle(h[1]);
CloseHandle(h[1]);
DeleteCriticalSection(&csObj);
return 0;
}
return 0;
}
B) Mutex:
a) Mutex prevents the simultaneous execution of a block of code by more than one thread at a time.
b) Mutex is a shortened form of the term "mutually exclusive".
c) Mutex can be used to synchronize threads across processes.
d) When used for inter-process synchronization, a mutex is called a named mutex because it is to be used in another application, and therefore it cannot be shared by means of a global or static variable. It must be given a name so that both applications can access the same mutex object.
Creates or opens a named or unnamed mutex object:
HANDLE CreateMutexA(
[in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
[in] BOOL bInitialOwner,
[in, optional] LPCSTR lpName
);
Releases ownership of the specified mutex object:
BOOL ReleaseMutex(
[in] HANDLE hMutex
);
C) Semaphore:
A semaphore object is a synchronization object that maintains a count between zero and a specified maximum value. The count is decremented each time a thread completes a wait for the semaphore object and incremented each time a thread releases the semaphore. When the count reaches zero, no more threads can successfully wait for the semaphore object state to become signaled. The state of a semaphore is set to signaled when its count is greater than zero, and nonsignaled when its count is zero.
Create a semaphore with initial and max counts of MAX_SEM_COUNT:
HANDLE CreateSemaphoreA(
[in, optional] LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // default security attributes
[in] LONG lInitialCount, // initial count
[in] LONG lMaximumCount, // maximum count
[in, optional] LPCSTR lpName // named semaphore
);
Try to enter the semaphore gate (The semaphore object was signaled):
WaitForSingleObject(
ghSemaphore, // handle to semaphore
0L); // zero-second time-out interval
Release the semaphore when task is finished:
BOOL ReleaseSemaphore(
[in] HANDLE hSemaphore,
[in] LONG lReleaseCount,
[out, optional] LPLONG lpPreviousCount
);
Example:
#include <windows.h>
#include <stdio.h>
#include <stdio.h>
#define MAX_SEM_COUNT 3
#define THREADCOUNT 5
HANDLE ghSemaphore;
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
// lpParam not used in this example
UNREFERENCED_PARAMETER(lpParam);
DWORD dwWaitResult;
BOOL bContinue = TRUE;
DWORD tID = GetCurrentThreadId();
cout << "Current Thread ID: " << tID << endl;
while (bContinue)
{
// Try to enter the semaphore gate.
dwWaitResult = WaitForSingleObject(
ghSemaphore, // handle to semaphore
INFINITE); // zero-second time-out interval //INFINITE
switch (dwWaitResult)
{
// The semaphore object was signaled.
case WAIT_OBJECT_0:
// TODO: Perform task
printf("Thread %d: wait succeeded\n", GetCurrentThreadId());
for (int i = 0; i < 100; i++)
{
cout << "CurrentThreadID = " << tID << " : MyThreadFunction[" << i << "]" << endl;
Sleep(100);
}
bContinue = FALSE;
// Release the semaphore when task is finished
if (!ReleaseSemaphore(
ghSemaphore, // handle to semaphore
1, // increase count by one
NULL)) // not interested in previous count
{
printf("ReleaseSemaphore error: %d\n", GetLastError());
}
break;
if (!ReleaseSemaphore(
ghSemaphore, // handle to semaphore
1, // increase count by one
NULL)) // not interested in previous count
{
printf("ReleaseSemaphore error: %d\n", GetLastError());
}
break;
// The semaphore was nonsignaled, so a time-out occurred.
case WAIT_TIMEOUT:
printf("Thread %d: wait timed out\n", GetCurrentThreadId());
break;
}
}
return TRUE;
}
case WAIT_TIMEOUT:
printf("Thread %d: wait timed out\n", GetCurrentThreadId());
break;
}
}
return TRUE;
}
int main(void)
{
HANDLE aThread[5];
DWORD ThreadID;
int i;
// Create a semaphore with initial and max counts of MAX_SEM_COUNT
ghSemaphore = CreateSemaphore(
NULL, // default security attributes
1, // initial count
3, // maximum count
NULL); // unnamed semaphore
ghSemaphore = CreateSemaphore(
NULL, // default security attributes
1, // initial count
3, // maximum count
NULL); // unnamed semaphore
if (ghSemaphore == NULL)
{
printf("CreateSemaphore error: %d\n", GetLastError());
return 1;
}
// Create worker threads
for (i = 0; i < 5; i++)
{
aThread[i] = CreateThread(
NULL, // default security attributes
0, // default stack size
(LPTHREAD_START_ROUTINE)ThreadProc,
NULL, // no thread function arguments
0, // default creation flags
&ThreadID); // receive thread identifier
for (i = 0; i < 5; i++)
{
aThread[i] = CreateThread(
NULL, // default security attributes
0, // default stack size
(LPTHREAD_START_ROUTINE)ThreadProc,
NULL, // no thread function arguments
0, // default creation flags
&ThreadID); // receive thread identifier
if (aThread[i] == NULL)
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
}
// Wait for all threads to terminate
WaitForMultipleObjects(5, aThread, TRUE, INFINITE);
WaitForMultipleObjects(5, aThread, TRUE, INFINITE);
// Close thread and semaphore handles
for (i = 0; i < 5; i++)
CloseHandle(aThread[i]);
for (i = 0; i < 5; i++)
CloseHandle(aThread[i]);
CloseHandle(ghSemaphore);
return 0;
}
}
D) Events:
Applications can use event objects in a number of situations to notify a waiting thread of the occurrence of an event.
Manual Reset Event: Manually reset to non-signaled after any waiting thread has been released.
Auto Reset Event: Automatically reset to non-signaled after any waiting thread has been released.
HANDLE CreateEventA(
[in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes, // Default security attributes
[in] BOOL bManualReset, // True - Manual-reset event; False - Auto-reset event
[in] BOOL bInitialState, // Initial state; TRUE - signaled; FALSE - nonsignaled
[in, optional] LPCSTR lpName // named event
);
SetEvent: Sets the state of the event object to signaled.
BOOL SetEvent(
[in] HANDLE hEvent
);
ResetEvent: Sets the state of the event object to non-signaled.
BOOL ResetEvent(
[in] HANDLE hEvent
);
Example:
#include <iostream>
#include <windows.h>
using namespace std;
#include <windows.h>
using namespace std;
HANDLE g_hEvent;
DWORD WINAPI MyThreadFunction1(LPVOID lpParam)
{
// Wait for the event to be signaled
DWORD dwWaitResult = WaitForSingleObject(g_hEvent, INFINITE);
if (dwWaitResult == WAIT_OBJECT_0)
{
std::cout << "Worker thread: Event signaled! Proceeding..." << std::endl;
// Perform some work after the event is signaled
for (int i = 0; i < 100; i++)
{
cout << "MyThreadFunction1[" << i << "]" << endl;
}
}
else
{
std::cerr << "Worker thread: WaitForSingleObject failed with error " << GetLastError() << std::endl;
}
return 0;
}
int main()
{
g_hEvent = CreateEvent(
NULL, // Default security attributes
TRUE, // Manual-reset event
FALSE, // Initial state is nonsignaled
NULL // Unnamed event
);
if (g_hEvent == NULL)
{
std::cerr << "CreateEvent failed with error " << GetLastError() << std::endl;
return 1;
}
DWORD dwThreadId;
LPVOID param;
HANDLE h;
h = CreateThread(NULL, 0, MyThreadFunction1, 0, 0, &dwThreadId);
// Signal the event, releasing any waiting threads
if (!SetEvent(g_hEvent))
{
std::cerr << "SetEvent failed with error " << GetLastError() << std::endl;
}
for (int i = 0; i < 100; i++)
{
cout << "main[" << i << "]" << endl;
}
WaitForSingleObject(g_hEvent, 1000);
CloseHandle(g_hEvent);
return 0;
}
Wait Functions:
Wait functions allow a thread to block its own execution. The wait functions do not return until the specified criteria have been met.
Single-object Wait Functions:
DWORD WaitForSingleObject(
[in] HANDLE hHandle, //A handle to the object
[in] DWORD dwMilliseconds //The time-out interval, in milliseconds
);
a) The WaitForSingleObject function checks the current state of the specified object.
b) If the object's state is nonsignaled, the calling thread enters the wait state until the object is signaled or the time-out interval elapses.
c) The time-out interval can be set to INFINITE to specify that the wait will not time out.
{
g_hEvent = CreateEvent(
NULL, // Default security attributes
TRUE, // Manual-reset event
FALSE, // Initial state is nonsignaled
NULL // Unnamed event
);
if (g_hEvent == NULL)
{
std::cerr << "CreateEvent failed with error " << GetLastError() << std::endl;
return 1;
}
DWORD dwThreadId;
LPVOID param;
HANDLE h;
h = CreateThread(NULL, 0, MyThreadFunction1, 0, 0, &dwThreadId);
// Signal the event, releasing any waiting threads
if (!SetEvent(g_hEvent))
{
std::cerr << "SetEvent failed with error " << GetLastError() << std::endl;
}
for (int i = 0; i < 100; i++)
{
cout << "main[" << i << "]" << endl;
}
WaitForSingleObject(g_hEvent, 1000);
CloseHandle(g_hEvent);
return 0;
}
Wait Functions:
Wait functions allow a thread to block its own execution. The wait functions do not return until the specified criteria have been met.
Single-object Wait Functions:
DWORD WaitForSingleObject(
[in] HANDLE hHandle, //A handle to the object
[in] DWORD dwMilliseconds //The time-out interval, in milliseconds
);
a) The WaitForSingleObject function checks the current state of the specified object.
b) If the object's state is nonsignaled, the calling thread enters the wait state until the object is signaled or the time-out interval elapses.
c) The time-out interval can be set to INFINITE to specify that the wait will not time out.
Multiple-object Wait Functions:
DWORD WaitForMultipleObjects(
[in] DWORD nCount, //The number of object handles in the array pointed to by lpHandles
[in] const HANDLE *lpHandles, //An array of object handles
[in] BOOL bWaitAll, //TRUE - wait until all are signaled
[in] DWORD dwMilliseconds //The time-out interval, in milliseconds
);
a) Waits until one or all of the specified objects are in the signaled state or the time-out interval elapses.
b) The time-out interval can be set to INFINITE to specify that the wait will not time out.
CloseHandle function:
[in] DWORD nCount, //The number of object handles in the array pointed to by lpHandles
[in] const HANDLE *lpHandles, //An array of object handles
[in] BOOL bWaitAll, //TRUE - wait until all are signaled
[in] DWORD dwMilliseconds //The time-out interval, in milliseconds
);
a) Waits until one or all of the specified objects are in the signaled state or the time-out interval elapses.
b) The time-out interval can be set to INFINITE to specify that the wait will not time out.
CloseHandle function:
Closes an open object handle.
BOOL CloseHandle([in] HANDLE hObject);
Refer: https://learn.microsoft.com/en-us/windows/win32/sync/using-synchronization
BOOL CloseHandle([in] HANDLE hObject);
Refer: https://learn.microsoft.com/en-us/windows/win32/sync/using-synchronization