Monday, November 25, 2024

Win32 Synchronization Objects

Overview
The following discusses concurrency objects that are available in Windows win32 API and from standard library.

Details
The following discusses each feature in detail. Note that the source code can be compiled using Visual Studio C++ compiler. All the related structures and classes are defined in windows.h file.
Some of these objects are kernel based and others are user based.

Classification
The objects are categorized into two categories  

NameDescription
Execution object Responsible for executing code
Synchronization object Responsible for providing synchronization between Execution objects.

Execution objects
These objects execute code in threads, thread pools APC queues.

NameDescription
ThreadA basic and  independent unit of execution.
Asynchronous Procedure CallEvery thread has a APC queue where APC calls can be queued.
Thread PoolInstead of creating threads applications can use thread pools provided by the Windows OS to execute short tasks.

Thread
A Thread is an independent, sequential flow of tasks with in a process. A process has at least one thread aka main thread. Multiple threads can be spawned on need basis.
A thread can be created using CreateThread(). It returns a thread handle that can be used for synchronization uses by calling Waitxxx() functions. After the thread function terminates, the thread handle must be closed by calling CloseHandle()
Often times, a thread's execution needs to be paused while other tasks needs to complete. For example, the main thread needs to wait for all the worker threads it has spawned are done. In such cases Waitxxx() functions such as WaitForSingleObject()WaitForMultipleObjects() are used. During the wait, the threads are not scheduled.

In this example, the main  thread spawns a worker thread that prints numbers 1 to 25 serially on the console as seen in its console output.

Asynchronous Procedure Call
Every thread has a APC queue where APC calls can be queued. APC calls can be in user mode or kernel mode. In order for a user mode APC call to execute, the thread must be in alertable state. 
A thread can be put in alertable state by calling APIs like SleepEx()WaitEx() functions etc. APC calls can be queued by calling QueueUserAPC(). After all the queued APCs are executed, the thread steps out of the waitable state.

In this example 9 , the main  thread spawns a worker threads. The worker thread sleeps in alertable state. APC calls are queued to print as in its console output.  The thread terminates after executing APC calls.

Thread Pool
Instead of creating threads, applications can use thread pools provided by the Windows OS to execute short tasks. Every process is associated with a thread pool.  Starting with windows vista, it's possible to customize the thread pool to the application's requirements. 
InitializeThreadpoolEnvironment() must be called before using the thread pool. A task can be queued to thread pool by calling 
TrySubmitThreadpoolCallback(). A callback and optional parameter can be provided to this API. Prior to Vista, applications used  QueueUserWorkItem() to queue a work item to the thread pool. It has similar signature as TrySubmitThreadpoolCallback(). Now this is supported only for backward compatibility.

In this  example 11, the main  thread queues work items using TrySubmitThreadpoolCallback() to the thread pool to print numbers serially as in its console output.  

In this legacy example, example 12, the main  thread queues work items using QueueUserWorkItem() to the thread pool to print numbers serially as in its console output.  

Synchronization objects
These objects are used by execution objects to provide synchronizations within a process or across. Some provide specialization for reader-writer and producer - consumer scenarios.

NameDescription
Critical Sectionlock that can be used within the same process to give access to a resource between threads.
Mutexmutually exclusive lock that can be used across processes to give access to a resource between threads.
Eventone or more threads wait on event object gets released when the event object is signalled.
Semaphoreallows upto n threads to access a resource simultaneously.
Slim Reader/WriterSimilar to critical sections that are designed to address reader/writer scenario.
Conditional VariableDesigned to address producer/consumer scenario.
WaitOnAddresslight weight mechanism that can be used support producer - consumer scenario.
InitOnceOne time initialization of variables that's used in multiple threads.
InterlockedThe Interlocked APIs provide a large range of atomic operations.

Critical Section
Critical sections are light weight synchronization objects that allow only one thread to execute a piece of code at a time within a process. Win32 provides APIs for a thread to spin for spin count times before entering for wait. 
Critical sections are created by instantiating CRITICAL_SECTION structure. Before it must be initialized by calling InitCriticalSection(). Inside the thread function, threads wait by calling  EnterCriticalSection() to execute the critical code. ExitCriticalSection() must be called  after exiting the critical code. When no longer needed, the critical section must be deleted by calling  DeleteCriticalSection().
For example, a thread B needs to wait on a critical section to access a resource while the thread A is using it. In such cases, the thread B is not scheduled until it's woken up after thread A  releases the critical section when it's done with the resource.
Note that Critical Section can be recursively locked. That means, a thread owning critical section can own it again without wait.

In this example 2, the main  thread spawns two worker threads that prints a  number serially on the console as seen in its console output. Serialization is achieved by a RAII(resource acquisition is initialization) object that uses a CRITICAL_SECTION.

Mutex
Mutex is Mutually Exclusive  synchronization object similar to critical section that allow only one thread to acquire a resource at a time across processes. 
Mutex can be created using CreateMutex() optionally with a name.  An existing mutex can be accessed by OpenMutex(). A thread calls one of the  Wait functions to acquire the resource. After the use of the resource,  ReleaseMutex() should be called. When the mutex is no longer needed, CloseHandle() should be used.
Note that Mutex can be recursively locked. That means, a thread owning mutex can own it again without wait.

In this example 3, the main  thread spawns two worker threads that prints a  number serially on the console as seen in its console output. Serialization is achieved by a RAII(resource acquisition is initialization) object that uses a mutex.

Event
Events are synchronization objects that can be configured as either manual or automatic. When a manual event object is signaled,  all the waiting threads will be released and stays signalled. In case of automatic, only one thread is released and becomes non signaled. An event object can be named and works across processes. 
Event object can be created using CreateEvent().  An existing event object can be accessed by OpenEvent(). A thread calls one of the  Waitxxx() functions to acquire the resource. PulseEvent() or SetEvent() releases the waiting threads. ResetEvent() puts back manual event object to non signaled state.

In this example 4, the main  thread spawns two worker threads that prints a  number serially on the console as shown in its console output. Serialization is achieved by a RAII(resource acquisition is initialization) object that uses a automatic reset event object.

Semaphore
Semaphores are synchronization objects that can be used to give access to one or more threads to a resource. Unlike mutex or event, semaphores are configured with a count. The count is decremented when a thread is given access. When the count reaches 0, semaphore stays signaled  and a thread needs to wait. After the use, a thread can release the semaphore. This will increase the count and any waiting threads will be released. Note that the semaphore locking is not mutually exclusive. i.e., any thread can release the semaphore; not just the threads that were released.
A semaphore object can be named and work across processes. Semaphore object can be created using CreateSemaphore().  An existing semaphore object can be accessed by OpenSemaphore(). A thread calls one of the  Wait functions to acquire the resource. After the use a thread can ReleaseSemaphore() and increase the count to releases the waiting threads. 

In this example 5 , the main  thread spawns two worker threads that prints a  number serially on the console as in its console output. Serialization is achieved by a RAII(resource acquisition is initialization) object that uses a Semaphore.

Slim Reader/Writer (SRW) Locks
SRW locks are light weight synchronization objects similar to critical section but with a difference. SRW locks are designed to address reader/writer scenario to a resource where multiple readers that don't modify can use shared lock to the resource where as writers can use exclusive locks to the resource. SRW locks can also be used with conditional variables discussed in another topic below.
SRW locks are created by instantiating SRWLOCK structure. Before it must be initialized by calling InitializeSRWLock(). Inside the thread function, threads wait by calling either  AcquireSRWLockShared() or AcquireSRWLockExclusive()  to execute the critical code.
ReleaseSRWLockShared() or ReleaseSRWLockExclusive() must be called  after exiting the critical code. When used with a conditional variable, SleepConditionVariableSRW() should be used to perform conditional sleep.

In this example 6, the main  thread spawns two worker threads -
  • Incrementer thread that acquires exclusive SRW lock, increments the counter ,and goes to fixed timed sleep.
  • ConsoleWriter thread that acquires shared SRW lock, prints the  counter value on the console as in its console output, and goes to fixed timed sleep.
Serialization is achieved by a RAII(resource acquisition is initialization) object that uses a SRWLock.

Conditional Variable
Unlike previously discussed synchronization objects, conditional variables are provided to support producer - consumer scenario. Typically  the producer thread after fulfillment, wakes up consumer thread from its conditional sleep and goes to conditional sleep. The consumer thread after consumption, wakes up producer thread from its conditional sleep and goes to conditional sleep.  CriticalSection  or SRWLock can be used for synchronization along with conditional variables. Like CriticalSections, conditional variables cannot be shared across processes.
InitializeConditionVariable() should be called to initialize the conditional variable. After fulfillment or consumption,  WakeConditionVariable() should be called to wake up the other party. SleepConditionVariableCS() should be called to do conditional sleep to be woken up. A CriticalSection is used to guard the critical code of fulfillment or consumption. When a thread goes to conditional sleep, the CriticalSection is also released.

In this example 7, the main  thread spawns two worker threads -
  • Producer thread that increments the counter, wakes up consumer thread and goes to conditional sleep.
  • consumer thread  that prints a  counter value on the console as in its console output, wakes up producer thread and goes to conditional sleep.
Serialization is achieved by two  RAII(resource acquisition is initialization) objects that uses a CriticalSection.

WaitOnAddress
WaitOnAddress are light weight mechanism that can be used support producer - consumer scenario similar to conditional variables. WaitOnAddress  uses memory variables such as global variables. 
During the execution of WaitOnAddress, a thread waits if the variable has the same value of the compared. The thread can be woken up by WakeByAddressSingle() call.
Typically  the producer thread after fulfillment, wakes up consumer thread from its address wait and goes to address wait. The consumer thread after consumption, wakes up producer thread from its address wait and goes to address wait. WaitOnAddress cannot be shared across processes.

In this example 8, the main  thread spawns two worker threads -
  • Producer thread that increments the counter, wakes up consumer thread and goes to address wait.
  • consumer thread  that prints a  counter value on the console as in its console output, wakes up producer thread and goes to address wait.
InitOnce
Some multi threaded application require initialization of variables that's used in multiple threads to be done only once. For example, a mutex needs to be created only once. Windows OS provides such facility to be performed either synchronously or asynchronously. 
In synchronous method, InitOnceExecuteOnce() should be called. A callback function is passed as a parameter  to this function. This callback function does the actual initialization. Windows OS ensures that callback function is done executed only once even when InitOnceExecuteOnce() is called from multiple threads.

In this example 10 , the main  thread spawns two worker threads that prints a  number serially on the console as in its console output. Serialization is achieved by a RAII(resource acquisition is initialization) object that uses a mutex. 
Both the worker threads call InitOnceExecuteOnce() but only one thread executes the callback that actually creates the mutex.

Interlocked
The Interlocked APIs provide a large range of atomic operations.
In this example 13, the main  thread spawns two worker threads that prints a  number serially on the console as seen in its console output. Serialization is achieved by calling interlocked add function.

Summary of Examples
The source of all the win32 examples are available in github and Rextester.
In Rextester examples can be viewed. Subscription is needed to edit, build and run.

NameSynchronization ObjectGithubRextester
Example    Thread  source   output    source+output   
Example 2Critical Section  source   output   source+output
Example 3Mutex  source   output  source+output
Example 4Event  source   output  source+output
Example 5Semaphore  source   output  source+output
Example 6Slim Reader/Writer  source   output  source+output
Example 7Conditional Variable  source   output  source+output
Example 8WaitOnAddress  source   output  source+output
Example 9Asynchronous Procedure Call  source   output  source+output
Example 10InitOnce  source   output  source+output
Example 11Thread Pool  source   output  source+output
Example 12Legacy Thread Pool  source   output  source+output
Example 13  Interlocked  source   output  source+output

No comments:

Post a Comment