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.

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;

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);
            
            return 0;
}

int main()

{
            InitializeCriticalSection(&csObj);
            
            DWORD dwThreadId[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);
            
            if (h[0])
                    CloseHandle(h[0]);
            
            if (h[1])
                    CloseHandle(h[1]);
            
            DeleteCriticalSection(&csObj);
            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>

#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;
            
            // The semaphore was nonsignaled, so a time-out occurred.
            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

        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

                    if (aThread[i] == NULL)

                    {
                            printf("CreateThread error: %d\n", GetLastError());
                            return 1;
                    }
        }
        
        // Wait for all threads to terminate
        WaitForMultipleObjects(5, aThread, TRUE, INFINITE);
        
        // Close thread and semaphore handles
        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;

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.

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:
Closes an open object handle.
BOOL CloseHandle([in] HANDLE hObject);

Refer: https://learn.microsoft.com/en-us/windows/win32/sync/using-synchronization

Popular posts from this blog

OBJECT ORIENTED ANALYSIS AND DESIGN (OOAD)

OBJECT ORIENTED PROGRAMMING

STARTING A BUSINESS