C++ classes for Working with Threads, Processes and Kernel Mode Synchronization.
The Harlinn.Common.Core library comes with C++ classes that simplifies multithreaded
software development for Windows. Threads, Processes and Kernel Mode Synchronization
objects can easily be created an managed, using:
WaitableHandle
: Base class for waitable Windows kernel objectsEventWaitHandle
: A class for working with Windows kernel event objectsMutex
: A class for working with Windows kernel mutex objectsSemaphore
: A class for working with Windows kernel semaphore objectsWaitableTimer
: A class for working with Windows kernel waitable timer objectsThread
: A thread class, similar in spirit to std::thread, but derived from WaitableHandle since threads are also waitable Windows kernel objectsProcess
: A class for executing other processes. Like threads, processes are waitable Windows kernel objects
Kernel Object Synchronization
Synchronization can be performed using several synchronization mechanisms that are backed by Windows kernel object implementations. These are the most heavyweight synchronization mechanisms provided by the Win32/64 API, and the most versatile:
- They can be named, putting them in the kernel object namespace.
- They can be used for inter-process synchronization.
- They can be secured.
- Child processes can inherit handles from the processes that either opened them or created them.
- We can specify a timeout, in milliseconds, when waiting for a kernel object to enter its signaled state.
- They can have a lifetime beyond the process that created them.
The Win32/64 API allows us to use the following kernel object types with synchronization:
- Thread
- Process
- File and console standard input, output, and error streams
- Job
- Event
- Mutex
- Semaphore
- Waitable timer
WaitableHandle
WaitableHandle
is the base class for the classes that provide access to the kernel objects that
a program can wait on. It has a single data member:
class WaitableHandle
{
private:
HANDLE handle_;
public:
...
};
WaitableHandle
is move assignable, move constructible, but not copy assignable and not copy constructible.
The implementation ensures that the lifetime of the handle is properly managed. The class, as the name
suggests, implements functions that allows us to wait on a kernel object.
The Wait(…)
function is a thin wrapper around the WaitForSingleObject(…)
function:
bool Wait( UInt32 timeoutInMillis = INFINITE ) const
{
auto rc = WaitForSingleObject( handle_, timeoutInMillis );
if ( rc == WAIT_FAILED )
{
ThrowLastOSError( );
}
return rc == WAIT_TIMEOUT ? false : true;
}
The Wait(…)
function returns true
if the wait was successful, and the kernel object is in a signaled state,
or false
if the wait expired due to a timeout. If the WaitForSingleObject(…)
function returns WAIT_FAILED
indicating an error, Wait(…)
will throw an exception containing the error code returned by GetLastError()
and the accompanying error message provided by the OS.
The WaitForSingleObject(…)
function also works as memory barrier, so no further action is required by the
thread to ensure safe access to the objects protected by the handle.
To provide compatibility with the std::lock_guard
and other templates from the standard template library,
WaitableHandle
also implements:
void lock( ) const
{
Wait( );
}
bool try_lock( ) const
{
return Wait( );
}
It is up to the derived classes to implement the missing unlock( )
member function.
EventWaitHandle
The EventWaitHandle
class provides a mechanism to notify a waiting thread of the occurrence
of an event. This class wraps the Event kernel object and manage the lifetime of the handle.
There are two kinds of event objects: manual-reset and auto-reset. When an event enters a signaled state, a manual-reset event will release all the waiting threads for execution, while an auto-reset event will only release one of the waiting threads.
Events are often used when one thread performs some work and then signals another thread to perform work that depends on the work it has just performed. The event is created in a non-signaled state, and then after the thread completes its work, it sets the event to a signaled state. At this point, the waiting thread is released and can continue its operation.
The EventWaitHandle
can be used like this:
EventWaitHandle event1( true );
EventWaitHandle event2( true );
Thread thread( [&event1, &event2]( )
{
puts( "Background thread signalling event1" );
event1.Signal( );
puts( "Background thread waiting for event2" );
event2.Wait( );
} );
puts( "Main thread waiting for event1" );
event1.Wait( );
puts( "Main thread signalling event2" );
event2.Signal( );
puts( "Main thread waiting for background thread to terminate" );
thread.Wait( );
Outputs:
Main thread waiting for event1
Background thread signalling event1
Background thread waiting for event2
Main thread signalling event2
Main thread waiting for background thread to terminate
Constructors
EventWaitHandle
objects can be constructed in several ways, and the default
constructor creates an empty object that can be move assigned another EventWaitHandle
.
constexpr EventWaitHandle( ) noexcept;
explicit EventWaitHandle( bool manualReset, bool initialState = false,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit EventWaitHandle( LPCWSTR name, bool manualReset = true, bool initialState = false,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit EventWaitHandle( LPCSTR name, bool manualReset = true, bool initialState = false,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit EventWaitHandle( const std::wstring& name, bool manualReset = true,
bool initialState = false,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit EventWaitHandle( const std::string& name, bool manualReset = true,
bool initialState = false,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
If manualReset
is set to true
, the constructor creates a manual-reset event,
otherwise, it creates an auto-reset event.
initialState
specifies that the event will be created in a signaled state.
When a manual-reset event is signaled, it remains signaled until it is reset to
non-signaled by the ResetEvent()
function. All waiting threads, or threads that
later begin to wait for the event, will be released when the object’s state is signaled.
When an auto-reset event is signaled, it remains signaled until a single waiting thread is released, then the system automatically resets the state to non-signaled. If no threads are waiting, the auto-reset event remains signaled.
The name
argument specifies a name for the event and the length must not be greater
than MAX_PATH
. If the name matches an existing event object, a handle to that event
is created and the manualReset
and initialState
arguments are ignored.
securityAttributes
specifies the SECURITY_ATTRIBUTES
for the event. When the
securityAttributes
is nullptr
, the event gets a default security descriptor
with ACLs from the primary or impersonation token of the creating thread.
desiredAccess
specifies the access mask for the event using values from the
EventWaitHandleRights
enumeration which can be combined using the |
operator.
None
: No rightsDelete
: The right to delete a named eventReadPermissions
: The right to open and copy the access rules and audit rules for a named eventSynchronize
: The right to wait on a named eventChangePermissions
: The right to change the security and audit rules associated with a named eventTakeOwnership
: The right to change the owner of a named eventModify
: The right to set or reset the signaled state of a named eventFullControl
: The right to exert full control over a named event, and to modify its access rules and audit rules
OpenExisting and TryOpenExisting
To access an existing event object, we use the OpenExisting(…)
function, which has the following overloads:
static EventWaitHandle OpenExisting( LPCWSTR name,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::Synchronize | EventWaitHandleRights::Modify,
bool inheritHandle = false );
static EventWaitHandle OpenExisting( LPCSTR name,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::Synchronize | EventWaitHandleRights::Modify,
bool inheritHandle = false );
static EventWaitHandle OpenExisting( const std::wstring& name,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::Synchronize | EventWaitHandleRights::Modify,
bool inheritHandle = false );
static EventWaitHandle OpenExisting( const std::string& name,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::Synchronize | EventWaitHandleRights::Modify,
bool inheritHandle = false );
The OpenExisting(…)
functions will throw an exception if the event cannot be
opened, while the TryOpenExisting(…)
functions will return an empty EventWaitHandle
.
static EventWaitHandle TryOpenExisting( LPCWSTR name,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::Synchronize | EventWaitHandleRights::Modify,
bool inheritHandle = false );
static EventWaitHandle TryOpenExisting( LPCSTR name,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::Synchronize | EventWaitHandleRights::Modify,
bool inheritHandle = false );
static EventWaitHandle TryOpenExisting( const std::wstring& name,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::Synchronize | EventWaitHandleRights::Modify,
bool inheritHandle = false );
static EventWaitHandle TryOpenExisting( const std::string& name,
EventWaitHandleRights desiredAccess = EventWaitHandleRights::Synchronize | EventWaitHandleRights::Modify,
bool inheritHandle = false );
PulseEvent
The PulseEvent()
function sets the event to the signaled state and then resets it to the
non-signaled state after releasing the waiting threads as specified by the event type.
void PulseEvent( ) const;
A manual-reset event will release all the threads that can be released immediately. The function then resets the state of the event to non-signaled and returns.
An auto-reset event will release a single waiting thread and then reset the state of the event to non-signaled.
According to the documentation for the PulseEvent(…)
Windows API function, this
function should be avoided:
A thread waiting on a synchronization object can be momentarily removed from the wait state by a kernel-mode APC, and then returned to the wait state after the APC is complete. If the call to PulseEvent occurs during the time when the thread has been removed from the wait state, the thread will not be released because PulseEvent releases only those threads that are waiting at the moment it is called. Therefore, PulseEvent is unreliable and should not be used by new applications.
SetEvent and Signal
SetEvent
and Signal
sets the signaled state of the event object.
void SetEvent( ) const;
void Signal( ) const;
Signal
just calls SetEvent
, but using it can often make the intent behind the
calling code clearer.
Setting an event that is already in the signaled state has no effect.
A manual-reset event remains signaled until it is set explicitly to the non-signaled
state by a call to the ResetEvent()
function. Waiting threads, and threads that begin
a wait operation for the event, will be released while the state of the event is signaled.
An auto-reset event is signaled until a single waiting thread is released, it is then reset to non-signaled automatically.
ResetEvent, Reset and unlock
ResetEvent
, Reset
and unlock
sets the event to a non-signaled state.
void ResetEvent( ) const;
void Reset( ) const;
void unlock( ) const;
Reset
and unlock
just calls ResetEvent
. By implementing unlock, the class meets the
BasicLockable requirements,
allowing us to use templates such as std::lock_guard
to wait on the event and automatically
reset the event when the lock goes out of scope. Depending on the design, this may make sense.
Mutex
The Mutex
is used to ensure that a thread has mutual exclusive access to an object.
The thread that owns a mutex can perform multiple wait operations on the Mutex
without
blocking its execution. This prevents the thread from deadlocking while waiting for a mutex
that it owns. To release ownership of the mutex, the thread must call ReleaseMutex
once
for each successful wait operation.
The Mutex
is a synchronization object that is set to signaled when it is not owned by a
thread, and non-signaled when it is owned.
The Mutex
class wraps the mutex kernel object and manage the lifetime of the handle.
The class meets the BasicLockable requirements,
allowing us to use templates such as std::unique_lock
to acquire and release ownership
of a mutex kernel object.
The Mutex
class can be used like this:
size_t counter = 0;
Mutex mutex( true );
ThreadGroup threadGroup;
for ( int i = 0; i < 100; ++i )
{
threadGroup.Add( [i, &mutex, &counter]( )
{
auto id = i + 1;
for ( int i = 0; i < 10; ++i )
{
printf( "T%d: waiting\n", id );
std::unique_lock lock( mutex );
printf( "T%d: acquired mutex\n", id );
++counter;
printf( "T%d: value %zu\n", id, counter );
}
} );
}
mutex.unlock( );
puts( "Main thread waiting on background threads" );
threadGroup.join();
printf( "Final value %zu\n", counter );
The example creates one hundred threads, where each thread takes a lock on the mutex and increments the value of counter ten times. The mutex is initially held by the main thread of the program and none of the “counter” threads can proceed until all the threads are created.
Output:
Main thread waiting on background threads
T1: waiting
T13: waiting
T2: waiting
T3: waiting
T4: waiting
T5: waiting
T6: waiting
T7: waiting
T20: waiting
...
...
T100: waiting
Main thread waiting on background threads
T83: acquired mutex
T83: value 2
T83: waiting
T76: acquired mutex
T76: value 3
T76: waiting
...
...
T3: value 998
T1: acquired mutex
T1: value 999
T2: acquired mutex
T2: value 1000
Final value 1000
Constructors
Mutex objects can be constructed in several ways, and the default constructor creates an empty object that can be move assigned another Mutex.
constexpr Mutex( ) noexcept;
explicit Mutex( bool initiallyOwned,
MutexRights desiredAccess = MutexRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit Mutex( LPCWSTR name, bool initiallyOwned = true,
MutexRights desiredAccess = MutexRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit Mutex( LPCSTR name, bool initiallyOwned = true,
MutexRights desiredAccess = MutexRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit Mutex( const std::wstring& name, bool initiallyOwned = true,
MutexRights desiredAccess = MutexRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit Mutex( const std::string& name, bool initiallyOwned = true,
MutexRights desiredAccess = MutexRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
If the caller created the mutex object and the initiallyOwned
argument is true
, the calling
thread gets ownership of the newly created mutex object. More than one process can call CreateMutex
,
or CreateMutexEx
which is called by the Mutex constructors, to create the same named mutex. The
first process will create the mutex, and the other processes will just open a handle to the existing
mutex. This allows multiple processes to get handles to the same mutex, without forcing the user
to make sure that the creating process is started first. If we do this, then we should set
initiallyOwned
to false to avoid uncertainty about which process has the initial ownership.
The name
argument specifies a name for the mutex object and the length must not be greater
than MAX_PATH
. If the name matches an existing mutex object, a handle to that mutex is created
and the initiallyOwned
argument is ignored.
securityAttributes
specifies the SECURITY_ATTRIBUTES
for the mutex. When the securityAttributes
is nullptr
, the mutex gets a default security descriptor with ACLs from the primary or impersonation
token of the creating thread.
desiredAccess specifies the access mask for the mutex using values from the MutexRights enumeration that can be combined using the ‘ |
’ operator. |
None
: No rightsDelete
: The right to delete a named mutexReadPermissions
: The right to open and copy the access rules and audit rules for a named mutexSynchronize
: The right to wait on a named mutexChangePermissions
: The right to change the security and audit rules associated with a named mutexTakeOwnership
: The right to change the owner of a named mutexModify
: The right to set or reset the signaled state of a named mutexFullControl
: The right to exert full control over a named mutex, and to modify its access rules and audit rules
OpenExisting and TryOpenExisting
To access an existing mutex object, we use the OpenExisting(…)
function, which has the following overloads:
static Mutex OpenExisting( LPCWSTR name,
MutexRights desiredAccess = MutexRights::Synchronize | MutexRights::Modify,
bool inheritHandle = false );
static Mutex OpenExisting( LPCSTR name,
MutexRights desiredAccess = MutexRights::Synchronize | MutexRights::Modify,
bool inheritHandle = false );
static Mutex OpenExisting( const std::wstring& name,
MutexRights desiredAccess = MutexRights::Synchronize | MutexRights::Modify,
bool inheritHandle = false );
static Mutex OpenExisting( const std::string& name,
MutexRights desiredAccess = MutexRights::Synchronize | MutexRights::Modify,
bool inheritHandle = false );
The OpenExisting(…)
functions will throw an exception if the mutex cannot be opened, while
the TryOpenExisting(…)
functions will return an empty Mutex.
static Mutex TryOpenExisting( LPCWSTR name,
MutexRights desiredAccess = MutexRights::Synchronize | MutexRights::Modify,
bool inheritHandle = false );
static Mutex TryOpenExisting( LPCSTR name,
MutexRights desiredAccess = MutexRights::Synchronize | MutexRights::Modify,
bool inheritHandle = false );
static Mutex TryOpenExisting( const std::wstring& name,
MutexRights desiredAccess = MutexRights::Synchronize | MutexRights::Modify,
bool inheritHandle = false );
static Mutex TryOpenExisting( const std::string& name,
MutexRights desiredAccess = MutexRights::Synchronize | MutexRights::Modify,
bool inheritHandle = false );
ReleaseMutex, Release and unlock
ReleaseMutex()
releases ownership of the mutex, and the Release()
and unlock()
functions
just calls the ReleaseMutex()
function.
Semaphore
Semaphore
objects can be used for resource counting. A semaphore has a maximum count and a
current count. Use the maximum count to hold the maximum number of resources protected by the
semaphore, and the current count for the number of currently available resources.
The state of a semaphore is set to signaled when its count is greater than zero, and non-signaled when its count is zero.
Each successful wait on a Semaphore
will cause the count to be decremented by 1, and we
must call the Release(…)
function to increase the semaphore’s count by a specified amount.
The count can never be less than zero or greater than the maximum value.
The class meets the BasicLockable requirements,
allowing us to use templates such as std::unique_lock
to wait on a Semaphore and call ReleaseSemaphore(1)
when the lock goes out of scope.
Here is a “toy” resource manager demonstrating a typical use case for a Semaphore
object:
namespace ResourceManager
{
struct Resource
{
long long counter_ = 0;
};
class Resources
{
constexpr static size_t ResourceCount = 5;
Semaphore semaphore_;
Mutex mutex_;
std::array<Resource, ResourceCount> resources_;
std::list< Resource* > freeList_;
public:
Resources( )
: semaphore_( ResourceCount, ResourceCount ), mutex_( false )
{
for ( auto& r : resources_ )
{
freeList_.push_back( &r );
}
}
Resource* GetResource( )
{
if ( semaphore_.Wait( ) )
{
std::unique_lock lock( mutex_ );
auto* result = freeList_.back( );
freeList_.pop_back( );
return result;
}
return nullptr;
}
void Release( Resource* r )
{
std::unique_lock lock( mutex_ );
freeList_.push_back( r );
semaphore_.Release( );
}
size_t Sum( ) const
{
size_t result = 0;
for ( auto& r : resources_ )
{
result += r.counter_;
}
return result;
}
};
}
The Resources
class manages access to five Resource
objects.
The Semaphore
is used to provide notification to the waiting threads that a resource is available
for allocation, while the Mutex
is used to protect the list of free resources.
To try it out, we let one hundred threads share a Resources
object and get access to Resource
objects
as they become available:
using namespace ResourceManager;
Resources resources;
ThreadGroup threadGroup;
for ( int i = 0; i < 100; ++i )
{
threadGroup.Add( [i, &resources]( )
{
auto id = i + 1;
for ( int i = 0; i < 10; ++i )
{
printf( "T%d: waiting\n", id );
auto* r = resources.GetResource( );
printf( "T%d: acquired resource\n", id );
r->counter_++;
printf( "T%d: value %zu\n", id, r->counter_ );
resources.Release( r );
}
} );
}
puts( "Main thread waiting on background threads" );
threadGroup.join( );
auto sum = resources.Sum( );
printf( "Final value %zu\n", sum );
Output:
T1: waiting
T2: waiting
T1: acquired resource
T1: value 1
T3: waiting
T3: acquired resource
T3: value 2
T5: waiting
T5: acquired resource
T5: value 3
T9: waiting
T18: waiting
...
...
T100: acquired resource
T100: value 190
T92: acquired resource
T92: value 200
T98: acquired resource
T98: value 207
T74: acquired resource
T74: value 178
T81: acquired resource
T81: value 225
Final value 1000
Constructors
Semaphore objects can be constructed in several ways, and the default constructor creates an empty object that can be move assigned another Semaphore.
constexpr Semaphore( ) noexcept;
explicit Semaphore( long initialCount, long maximumCount,
SemaphoreRights desiredAccess = SemaphoreRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit Semaphore( LPCWSTR name, long initialCount, long maximumCount,
SemaphoreRights desiredAccess = SemaphoreRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit Semaphore( LPCSTR name, long initialCount, long maximumCount,
SemaphoreRights desiredAccess = SemaphoreRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit Semaphore( const std::wstring& name, long initialCount, long maximumCount,
SemaphoreRights desiredAccess = SemaphoreRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit Semaphore( const std::string& name, long initialCount, long maximumCount,
SemaphoreRights desiredAccess = SemaphoreRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
The initialCount
argument specifies the initial count for the Semaphore
object,
and must not be less than zero or greater than the maximumCount argument. A semaphore is
non-signaled when its count is zero and signaled when it is greater. The count is decremented
by one for each thread that successfully waits on a semaphore. The count is increased by calling the
ReleaseSemaphore(…)
function with the amount to increase the count by as its argument.
The maximumCount
argument specifies the maximum count for the Semaphore
object, which must be 1
or greater.
The name
argument specifies a name for the semaphore object and the length must not be
greater than MAX_PATH
. If the name matches an existing semaphore object, a handle to that
semaphore is created and the initialCount and maximumCount arguments are ignored.
securityAttributes
specifies the SECURITY_ATTRIBUTES
for the semaphore. When the securityAttributes
is nullptr
, the semaphore gets a default security descriptor with ACLs from the primary or
impersonation token of the creating thread.
desiredAccess specifies the access mask for the semaphore using values from the SemaphoreRights enumeration which can be combined using the ‘ |
’ operator. |
None
: No rightsDelete
: The right to delete a named semaphoreReadPermissions
: The right to open and copy the access rules and audit rules for a named semaphoreSynchronize
: The right to wait on a named semaphoreChangePermissions
: The right to change the security and audit rules associated with a named semaphoreTakeOwnership
: The right to change the owner of a named semaphoreModify
: The right to set or reset the signaled state of a named semaphoreFullControl
: The right to exert full control over a named semaphore, and to modify its access rules and audit rules
OpenExisting and TryOpenExisting
To access an existing semaphore object, we use the OpenExisting(…)
function,
which has the following overloads:
static Semaphore OpenExisting( LPCWSTR name,
SemaphoreRights desiredAccess = SemaphoreRights::Synchronize | SemaphoreRights::Modify,
bool inheritHandle = false );
static Semaphore OpenExisting( LPCSTR name,
SemaphoreRights desiredAccess = SemaphoreRights::Synchronize | SemaphoreRights::Modify,
bool inheritHandle = false );
static Semaphore OpenExisting( const std::wstring& name,
SemaphoreRights desiredAccess = SemaphoreRights::Synchronize | SemaphoreRights::Modify,
bool inheritHandle = false );
static Semaphore OpenExisting( const std::string& name,
SemaphoreRights desiredAccess = SemaphoreRights::Synchronize | SemaphoreRights::Modify,
bool inheritHandle = false );
The OpenExisting(…)
functions will throw an exception if the semaphore cannot be opened,
while the TryOpenExisting(…)
functions will return an empty Semaphore
.
static Semaphore TryOpenExisting( LPCWSTR name,
SemaphoreRights desiredAccess = SemaphoreRights::Synchronize | SemaphoreRights::Modify,
bool inheritHandle = false );
static Semaphore TryOpenExisting( LPCSTR name,
SemaphoreRights desiredAccess = SemaphoreRights::Synchronize | SemaphoreRights::Modify,
bool inheritHandle = false );
static Semaphore TryOpenExisting( const std::wstring& name,
SemaphoreRights desiredAccess = SemaphoreRights::Synchronize | SemaphoreRights::Modify,
bool inheritHandle = false );
static Semaphore TryOpenExisting( const std::string& name,
SemaphoreRights desiredAccess = SemaphoreRights::Synchronize | SemaphoreRights::Modify,
bool inheritHandle = false );
ReleaseSemaphore, Release and unlock
ReleaseSemaphore
increases the count of the semaphore by the specified amount:
long ReleaseSemaphore( long releaseCount = 1 ) const;
long Release( long releaseCount = 1 ) const;
void unlock( ) const;
Release
just calls ReleaseSemaphore
, as does unlock()
with releaseCount
set to one.
WaitableTimer
A waitable timer is a synchronization object whose state is set to signaled when the specified due time arrives. The Windows API provides two waitable timer types: manual-reset and synchronization; and both can be periodic.
A manual-reset waitable timer is signaled until the SetWaitableTimer
function is called to set a
new due time; while a synchronization timer is in its signaled state until a single thread is
released after successfully waiting on the waitable timer.
The following fragment creates a thread that will wait for five seconds on the timer.
std::cout << "Start: " << DateTime::Now( ) << std::endl;
EventWaitHandle event( true );
WaitableTimer timer(true, TimeSpan::FromSeconds( 5 ) );
Thread thread( [&timer, &event]( )
{
std::cout << "Background thread waiting on timer" << std::endl;
timer.Wait( );
std::cout << "Background thread continued: " << DateTime::Now( ) << std::endl;
std::cout << "Background thread signalling event" << std::endl;
event.Signal( );
} );
std::cout << "Main thread waiting for event" << std::endl;
event.Wait( );
std::cout << "Main thread waiting for background thread to terminate" << std::endl;
thread.Wait( );
Outputs:
Start: 03.09.2020 22 : 02 : 59
Main thread waiting for event
Background thread waiting on timer
Background thread continued : 03.09.2020 22 : 03 : 04
Background thread signalling event
Main thread waiting for background thread to terminate
Constructors
WaitableTimer
objects can be constructed in several ways, and the default constructor creates
an empty object that can be move assigned another WaitableTimer
.
constexpr WaitableTimer( ) noexcept;
explicit WaitableTimer( bool manualReset,
WaitableTimerRights desiredAccess = WaitableTimerRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit WaitableTimer( bool manualReset, const DateTime& dueTime,
const TimeSpan& interval = TimeSpan( ),
WaitableTimerRights desiredAccess = WaitableTimerRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit WaitableTimer( bool manualReset, const TimeSpan& dueTime,
const TimeSpan& interval = TimeSpan( ),
WaitableTimerRights desiredAccess = WaitableTimerRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit WaitableTimer( LPCWSTR name, bool manualReset = true,
WaitableTimerRights desiredAccess = WaitableTimerRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit WaitableTimer( LPCSTR name, bool manualReset = true,
WaitableTimerRights desiredAccess = WaitableTimerRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit WaitableTimer( const std::wstring& name, bool manualReset = true,
WaitableTimerRights desiredAccess = WaitableTimerRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
explicit WaitableTimer( const std::string& name, bool manualReset = true,
WaitableTimerRights desiredAccess = WaitableTimerRights::FullControl,
LPSECURITY_ATTRIBUTES securityAttributes = nullptr );
If the manualReset
argument is true, the constructor creates a manual-reset waitable
timer; if false, a synchronization waitable timer is created that is automatically
reset after releasing a single waiting thread.
The dueTime
argument specifies when the waitable timer will enter the signaled state.
If dueTime
is a DateTime
, then the dueTime
is absolute, and when dueTime
is a TimeSpan
,
the dueTime is relative.
The name
argument specifies a name for the waitable timer object and the length must not be greater
than MAX_PATH
. If the name matches an existing waitable timer object, a handle to that waitable timer
is created and the manualReset
argument is ignored.
securityAttributes
specifies the SECURITY_ATTRIBUTES
for the waitable timer. When the
securityAttributes
is nullptr
, the waitable timer gets a default security descriptor
with ACLs from the primary or impersonation token of the creating thread.
desiredAccess
specifies the access mask for the waitable timer using values from the
WaitableTimerRights
enumeration that can be combined using the ‘|’ operator.
None
: No rightsDelete
: The right to delete a named waitable timerReadPermissions
: The right to open and copy the access rules and audit rules for a named waitable timerSynchronize
: The right to wait on a named waitable timerChangePermissions
: The right to change the security and audit rules associated with a named waitable timerTakeOwnership
: The right to change the owner of a named waitable timerModify
: The right to set or reset the signaled state of a named waitable timerFullControl
: The right to exert full control over a named waitable timer, and to modify its access rules and audit rules
OpenExisting and TryOpenExisting
To access an existing waitable timer object, we use the OpenExisting(…)
function, which
has the following overloads:
static WaitableTimer OpenExisting( LPCWSTR name,
WaitableTimerRights desiredAccess = WaitableTimerRights::Synchronize | WaitableTimerRights::Modify,
bool inheritHandle = false );
static WaitableTimer OpenExisting( LPCSTR name,
WaitableTimerRights desiredAccess = WaitableTimerRights::Synchronize | WaitableTimerRights::Modify,
bool inheritHandle = false );
static WaitableTimer OpenExisting( const std::wstring& name,
WaitableTimerRights desiredAccess = WaitableTimerRights::Synchronize | WaitableTimerRights::Modify,
bool inheritHandle = false );
static WaitableTimer OpenExisting( const std::string& name,
WaitableTimerRights desiredAccess = WaitableTimerRights::Synchronize | WaitableTimerRights::Modify,
bool inheritHandle = false );
The OpenExisting(…)
functions will throw an exception if the waitable timer cannot be opened, while
the TryOpenExisting(…)
functions will return an empty WaitableTimer
.
static WaitableTimer TryOpenExisting( LPCWSTR name,
WaitableTimerRights desiredAccess = WaitableTimerRights::Synchronize | WaitableTimerRights::Modify,
bool inheritHandle = false );
static WaitableTimer TryOpenExisting( LPCSTR name,
WaitableTimerRights desiredAccess = WaitableTimerRights::Synchronize | WaitableTimerRights::Modify,
bool inheritHandle = false );
static WaitableTimer TryOpenExisting( const std::wstring& name,
WaitableTimerRights desiredAccess = WaitableTimerRights::Synchronize | WaitableTimerRights::Modify,
bool inheritHandle = false );
static WaitableTimer TryOpenExisting( const std::string& name,
WaitableTimerRights desiredAccess = WaitableTimerRights::Synchronize | WaitableTimerRights::Modify,
bool inheritHandle = false );
SetTimer
The SetTimer
function activates the waitable timer, and when the due time arrives,
the waitable timer is signaled.
void SetTimer( LARGE_INTEGER dueTime, DWORD interval,
PTIMERAPCROUTINE completionRoutine,
void* argToCompletionRoutine,
bool resumeSystemIfSuspended ) const;
void SetTimer( LARGE_INTEGER dueTime, DWORD interval,
bool resumeSystemIfSuspended = false ) const;
void SetTimer( const DateTime& dueTime, const TimeSpan& interval,
PTIMERAPCROUTINE completionRoutine,
void* argToCompletionRoutine,
bool resumeSystemIfSuspended ) const;
void SetTimer( const TimeSpan& dueTime, const TimeSpan& interval,
PTIMERAPCROUTINE completionRoutine,
void* argToCompletionRoutine,
bool resumeSystemIfSuspended ) const;
void SetTimer( const DateTime& dueTime, const TimeSpan& interval = TimeSpan( ),
bool resumeSystemIfSuspended = false ) const;
void SetTimer( const TimeSpan& dueTime, const TimeSpan& interval = TimeSpan( ),
bool resumeSystemIfSuspended = false ) const;
The dueTime
specifies the time for when the timer will be set to signaled. Use a positive
value to set the absolute time in UTC as FILETIME, or a negative value to set the relative
time with a 100-nanosecond resolution. When dueTime
is given as a DateTime
, the argument
specifies an absolute dueTime, and when given as a TimeSpan
, the dueTime will be relative.
The interval
argument gives the period of the timer, in milliseconds. When interval
is zero,
the waitable timer will be signaled once, and when interval
is greater than zero, the waitable
timer will be periodic and automatically reactivated each time the period elapses, until the timer
is cancelled using the Cancel()
function or reset using the SetTimer(…)
function. When interval
is given as a TimeSpan
, the argument will be converted to milliseconds.
The completionRoutine
argument specifies a pointer to an optional completion routine.
The argToCompletionRoutine
argument specifies an argument to be passed to the optional completion routine.
If the resumeSystemIfSuspended
argument is true
, the system will be restored from suspended
power conservation mode when the waitable timer becomes signaled.
Thread
A thread is also a synchronization object that we can wait on. Thread
objects enter their signaled
state when they are done executing.
The Thread
class can be used like the std::thread
class, and provides additional Windows specific functions.
NOTE: We should not use the Win x86/x64 ExitThread(…)
or the c runtime _endthread(…)
or _endthreadex(…)
functions to terminate a thread, both this implementation and the implementation of std::thread
that is currently
provided with Visual C++ use a std::unique_ptr<>
to hold a pointer to a tuple<…>
containing the thread arguments,
so the stack needs to be properly unwound to allow the std::unique_ptr<> destructor to delete this tuple.
The Thread
class does provide an alternative that is implemented by throwing an exception that is not derived
from std::exception. This is not a bullet proof mechanism as any catch all, catch(…)
, will catch this exception.
The best way to exit a thread is to return from the thread function.
Thread thread( []( ) { return 5; } );
thread.join( );
auto exitCode = thread.ExitCode( );
printf( "Thread exited with exit code %d\n", exitCode );
Outputs:
Thread exited with exit code 5
The implementation of std::thread
provided with Visual C++ closes the handle to the thread
in its implementation of join( )
, making it impossible to perform further operations on
the object. The Thread
class will keep the handle until we call Close(), or the object goes out of scope.
Constructors
Thread
objects can be constructed in several ways, and the default constructor creates an
empty object that can be move assigned another Thread
.
constexpr Thread( ) noexcept;
constexpr Thread( HANDLE handle, UInt32 threadId ) noexcept;
template <class Function, class... Args>
requires ( std::is_same_v<std::remove_cvref_t<Function>, Thread> == false )
explicit Thread( Function&& function, Args&&... args );
template <class Function, class... Args>
requires ( std::is_same_v<std::remove_cvref_t<Function>, Thread> == false )
explicit Thread( LPSECURITY_ATTRIBUTES securityAttributes,
Function&& function, Args&&... args );
The second constructor initializes a Thread
object using the handle and threadId
arguments,
which are assumed to be valid.
The two last constructors create a new thread, executing std::invoke
using a decayed copy
of function and its decayed arguments that have been passed to the new thread.
securityAttributes
specifies the SECURITY_ATTRIBUTES
for the thread. When the securityAttributes
is nullptr
, the thread gets a default security descriptor with ACLs from the primary or impersonation
token of the creating thread.
Process
A process is also a synchronization object that we can wait on. Process
objects enter their signaled
state when they are done executing.