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
Name | Description |
---|---|
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.
Name | Description |
---|---|
Thread | A basic and independent unit of execution. |
Asynchronous Procedure Call | Every thread has a APC queue where APC calls can be queued. |
Thread Pool | Instead 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.
Name | Description |
---|---|
Critical Section | lock that can be used within the same process to give access to a resource between threads. |
Mutex | mutually exclusive lock that can be used across processes to give access to a resource between threads. |
Event | one or more threads wait on event object gets released when the event object is signalled. |
Semaphore | allows upto n threads to access a resource simultaneously. |
Slim Reader/Writer | Similar to critical sections that are designed to address reader/writer scenario. |
Conditional Variable | Designed to address producer/consumer scenario. |
WaitOnAddress | light weight mechanism that can be used support producer - consumer scenario. |
InitOnce | One time initialization of variables that's used in multiple threads. |
Interlocked | The 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.
- 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.
- 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.
- 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
In Rextester examples can be viewed. Subscription is needed to edit, build and run.
Name | Synchronization Object | Github | Rextester |
---|---|---|---|
Example | Thread | source output | source+output |
Example 2 | Critical Section | source output | source+output |
Example 3 | Mutex | source output | source+output |
Example 4 | Event | source output | source+output |
Example 5 | Semaphore | source output | source+output |
Example 6 | Slim Reader/Writer | source output | source+output |
Example 7 | Conditional Variable | source output | source+output |
Example 8 | WaitOnAddress | source output | source+output |
Example 9 | Asynchronous Procedure Call | source output | source+output |
Example 10 | InitOnce | source output | source+output |
Example 11 | Thread Pool | source output | source+output |
Example 12 | Legacy Thread Pool | source output | source+output |
Example 13 | Interlocked | source output | source+output |
No comments:
Post a Comment