This documents some of the design decisions made when implementing the C11 threading primitives on Windows
tss_t
Note: see C11 defect report: tss_t destruction unspecified for issues with the standard specification of tss_t. The PDCLib implementation at present follows that report.
Windows provides a thread local storage implementation which mostly implements the requirements of the standard. However, it doesn't implement destructors. To solve that, PDCLib uses two approaches
- TLS callbacks: Executables and DLLs can specify "TLS callbacks", which are actions invoked in a similar manner to (but before) the traditional DllMain entry point for a DLL - and, importantly, are present in executables (which do not support DllMain). This works in all cases on NT 6.0 and later; but on older versions of Windows, these will only be invoked for the main executable and any DLLs loaded as direct dependencies of it (i.e. nothing loaded dynamically with LoadLibrary)
- DllMain: This works for all libraries everywhere, but doesn't work for executables.
There are three cases to consider:
- Linked statically from the executable: In this case we must use TLS callbacks
- Linked statically from a DLL: In this case we must use DllMain for wide support
- Linked dynamically: In this case we use our own DllMain
Our implementation strategy:
- TBD
mtx_t
Our implementation doesn't use Win32 CRITICAL_SECTIONs; primarily because this would involve exposing <windows.h> to all people who include <threads.h>, or duplicating the definition of CRITICAL_SECTION (messy). Instead, we implement a locking primitive on top of Windows' event handles
- Every mutex has two members, _WaitEvHandle, which is a automatic reset Win32 event handle used for blocking, and _ThreadId, the ID of the current thread
- _State is initialized to 0
- To lock,
mtx_lock
calls InterlockedIncrement on _State. If the return value is 0, we successfully locked the mutex. If it is not 0, then we must block, so go ahead and block on the event handle - To unlock,
mtx_unlock
calls InterlockedDecrement on _State. If the return value is -1, nobody was waiting and we can return without touching the kernel; otherwise, we mustSetEvent
the event handle in order to unblock a thread - Note that at no point once someone has blocked on the mutex will the _State member return to -1 until all threads blocked on the mutex
Waits with timeouts
Timed waits must be careful about lowering the _State variable after their wait times out. In particular, if after the timeout _State == 0, then they actually just got granted the mutex - and, more importantly - the event object is set. When this occurs, they must do a wait with a timeout of zero on the event object and repeat this every time the wait times out until _State no longer equals 0 (which indicates somebody else took the mutex) or until the wait completes (indicating that they got the mutex)