The <threads.h> header, introduced in the C11 standard, provides the necessary programming interfaces to develop multi-threaded C code. Multi-threading breaks strict sequential execution into a more parallel and dynamically scheduled order that is non-deterministic. But a word of caution: multi-threaded code easily becomes a can of worms if not approached methodically and prudently. This is why <threads.h> offers mutexes, condition variables, and thread management functions to protect data dependencies in this multi-threaded world.
Cleanly testing thread library functions in isolation presents several challenges:
- Common thread functions must be used in pairs: lock/unlock, wait/signal, or wait/broadcast.
- While their interfaces are often simple, typically producing binary outcomes, their success or failure is often expressed in data synchronization with surrounding code and not in computing some value. Their intended behavior only becomes clear in a wide context, which is further subject to the C standard section regarding data race conditions.
- Their use might impose certain constraints on the user. Constraints not enforced by the standard that leave the program in an unpredictable state. Hence, isolated intra-thread testing must happen carefully within the boundaries of these constraints. Any violation of such constraints cannot be tested in itself.
As a result, testing thread functions involves integrating them into a practical, engineered multi-threaded scenario. Within this context, the test framework initializes mutexes and condition variables, creates multiple threads, manages their execution, and ensures correct synchronization so that tested threads can increment shared counters and contribute to a common result. Tests typically use these counters to ensure that each thread executes exactly once, does not violate protected data, and correctly synchronizes, verifying the correctness of intricate thread interactions.
To effectively manage complexity, the testing framework explores different thread interaction scenarios:
- Serialized: The threads use a single mutex to access shared resources sequentially, ensuring the absence of conflicts due to parallel access.
- Parallel: Threads independently operate on separate resources without mutexes, highlighting purely parallel and thread-safe execution.
- Mixed: Threads share a limited number of resources protected by resource mutexes, creating realistic concurrency and interaction scenarios.
Unlike conventional tests, multi-threading can introduce a degree of non-determinism especially under stress. This emphasizes the importance of carefully designed testing strategies that are both scalable and reliable. SuperGuard provides exactly that with its support for <threads.h>. It allows testing in any target context to help identify whether the implementation reliably handles heavy workloads, meets performance targets, and remains thread-safe under concurrent operations.
John van Brummen, Software Engineer