150 lines
4.9 KiB
C
150 lines
4.9 KiB
C
|
|
#ifndef AKERR_TEST_THREADS_H
|
||
|
|
#define AKERR_TEST_THREADS_H
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Shared helpers for the thread-safety tests.
|
||
|
|
*
|
||
|
|
* These tests are checkable on their own -- they assert exclusive ownership of
|
||
|
|
* pool slots and of reserved ranges, which is a property, not a symptom -- but
|
||
|
|
* the run that proves the absence of a data race is the one under
|
||
|
|
* ThreadSanitizer:
|
||
|
|
*
|
||
|
|
* cmake -S . -B build/tsan -DAKERR_SANITIZE=thread
|
||
|
|
* cmake --build build/tsan
|
||
|
|
* ctest --test-dir build/tsan --output-on-failure
|
||
|
|
*
|
||
|
|
* Everything shared between the threads here is either read-only after the
|
||
|
|
* threads start, or touched through __atomic builtins. Anything else would be a
|
||
|
|
* race in the *test*, and TSan cannot tell whose bug it is reporting.
|
||
|
|
*
|
||
|
|
* A test body runs on AKERR_TEST_THREADS threads that meet at a barrier first,
|
||
|
|
* so they arrive at the library together instead of in start-up order. Failed
|
||
|
|
* checks are counted per thread rather than returned early: a thread that
|
||
|
|
* abandoned its work would leave the others holding pool slots and turn one
|
||
|
|
* failure into a cascade of unrelated ones.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include "akerror.h"
|
||
|
|
#include <pthread.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
|
||
|
|
#define AKERR_TEST_THREADS 8
|
||
|
|
|
||
|
|
typedef struct
|
||
|
|
{
|
||
|
|
int id; /* 1-based: 0 means "no thread" below */
|
||
|
|
int failures;
|
||
|
|
pthread_barrier_t *barrier;
|
||
|
|
} akerr_ThreadArg;
|
||
|
|
|
||
|
|
#define AKERR_TCHECK(__arg, __cond) \
|
||
|
|
do { \
|
||
|
|
if ( !(__cond) ) { \
|
||
|
|
fprintf(stderr, "CHECK FAILED (thread %d): %s at %s:%d\n", \
|
||
|
|
(__arg)->id, #__cond, __FILE__, __LINE__); \
|
||
|
|
(__arg)->failures += 1; \
|
||
|
|
} \
|
||
|
|
} while ( 0 )
|
||
|
|
|
||
|
|
/*
|
||
|
|
* A logger that counts instead of printing. The capturing logger in
|
||
|
|
* err_capture.h appends to a shared buffer with a shared length, which is a
|
||
|
|
* data race the moment two threads log at once; these tests need a logger that
|
||
|
|
* is safe to install before spawning and still shows that something was
|
||
|
|
* reported.
|
||
|
|
*/
|
||
|
|
static int akerr_thread_log_count;
|
||
|
|
|
||
|
|
static void __attribute__((unused)) akerr_thread_logger(const char *fmt, ...)
|
||
|
|
{
|
||
|
|
(void)fmt;
|
||
|
|
__atomic_fetch_add(&akerr_thread_log_count, 1, __ATOMIC_RELAXED);
|
||
|
|
}
|
||
|
|
|
||
|
|
static int __attribute__((unused)) akerr_thread_logs(void)
|
||
|
|
{
|
||
|
|
return __atomic_load_n(&akerr_thread_log_count, __ATOMIC_RELAXED);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Independent bookkeeping of who holds which pool slot. The library's own
|
||
|
|
* refcount says a slot is checked out; this says which thread it was checked
|
||
|
|
* out to, which is the part a racing akerr_next_error() would get wrong by
|
||
|
|
* handing one slot to two threads at once.
|
||
|
|
*/
|
||
|
|
static int akerr_slot_owner[AKERR_MAX_ARRAY_ERROR];
|
||
|
|
|
||
|
|
/* Returns 0 on success, or the id of the thread that already holds the slot. */
|
||
|
|
static int __attribute__((unused)) akerr_slot_claim(int slot, int id)
|
||
|
|
{
|
||
|
|
int unowned = 0;
|
||
|
|
|
||
|
|
if ( __atomic_compare_exchange_n(&akerr_slot_owner[slot], &unowned, id, 0,
|
||
|
|
__ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE) ) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
return unowned;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int __attribute__((unused)) akerr_slot_holder(int slot)
|
||
|
|
{
|
||
|
|
return __atomic_load_n(&akerr_slot_owner[slot], __ATOMIC_ACQUIRE);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Give the slot up *before* releasing the error context. The other order hands
|
||
|
|
* the slot back to the pool while this thread still claims it, and the next
|
||
|
|
* thread to be given it reports a violation that is the test's fault.
|
||
|
|
*/
|
||
|
|
static void __attribute__((unused)) akerr_slot_drop(int slot)
|
||
|
|
{
|
||
|
|
__atomic_store_n(&akerr_slot_owner[slot], 0, __ATOMIC_RELEASE);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Run `body` on AKERR_TEST_THREADS threads and return the total number of
|
||
|
|
* failed checks. A thread that cannot be created is itself a failure, but the
|
||
|
|
* ones already running still get joined.
|
||
|
|
*/
|
||
|
|
static int __attribute__((unused)) akerr_run_threads(void *(*body)(void *))
|
||
|
|
{
|
||
|
|
pthread_t threads[AKERR_TEST_THREADS];
|
||
|
|
akerr_ThreadArg args[AKERR_TEST_THREADS];
|
||
|
|
pthread_barrier_t barrier;
|
||
|
|
int started = 0;
|
||
|
|
int failures = 0;
|
||
|
|
|
||
|
|
if ( pthread_barrier_init(&barrier, NULL, AKERR_TEST_THREADS) != 0 ) {
|
||
|
|
fprintf(stderr, "CHECK FAILED: pthread_barrier_init at %s:%d\n",
|
||
|
|
__FILE__, __LINE__);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
for ( int i = 0; i < AKERR_TEST_THREADS; i++ ) {
|
||
|
|
args[i].id = i + 1;
|
||
|
|
args[i].failures = 0;
|
||
|
|
args[i].barrier = &barrier;
|
||
|
|
if ( pthread_create(&threads[i], NULL, body, &args[i]) != 0 ) {
|
||
|
|
fprintf(stderr, "CHECK FAILED: pthread_create for thread %d"
|
||
|
|
" at %s:%d\n", i + 1, __FILE__, __LINE__);
|
||
|
|
failures++;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
started++;
|
||
|
|
}
|
||
|
|
/* An unstarted thread never reaches the barrier, so the started ones would
|
||
|
|
* wait for it forever. Nothing to do but say so before hanging is diagnosed
|
||
|
|
* as a deadlock in the library. */
|
||
|
|
if ( started != AKERR_TEST_THREADS ) {
|
||
|
|
fprintf(stderr, "only %d of %d threads started; the barrier will not"
|
||
|
|
" release\n", started, AKERR_TEST_THREADS);
|
||
|
|
}
|
||
|
|
for ( int i = 0; i < started; i++ ) {
|
||
|
|
pthread_join(threads[i], NULL);
|
||
|
|
failures += args[i].failures;
|
||
|
|
}
|
||
|
|
pthread_barrier_destroy(&barrier);
|
||
|
|
return failures;
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif // AKERR_TEST_THREADS_H
|