Make the error pool and status registry thread safe

Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.

One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.

This is an ABI break, hence 2.0.0 and SOVERSION 2:

- akerr_next_error() now returns a context that already holds its
  reference. Finding a free slot and claiming it has to be one operation,
  or two threads scanning at once are handed the same slot.
  ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
  to report akerr_release_error(NULL).

The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.

Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.

Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
2026-07-31 08:31:22 -04:00
parent 499384ed0d
commit 496cb58251
19 changed files with 1463 additions and 140 deletions

View File

@@ -1,15 +1,44 @@
#include "akerror.h"
#include "lock.h"
#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>
#endif // AKERR_USE_STDLIB
akerr_ErrorContext __akerr_last_ditch;
akerr_ErrorContext *__akerr_last_ignored;
/*
* Per-thread state.
*
* The last-ditch context is where a failure gets reported when there is no pool
* slot to report it from -- akerr_release_error(NULL). One shared copy would
* have two threads formatting a message into the same buffer, so each thread
* gets its own. Thread-local storage is zero initialized, which is why
* akerr_last_ditch_context() below sets the stack-trace cursor lazily rather
* than akerr_init() setting it for everyone: akerr_init() runs on one thread
* and cannot reach the others' copies.
*
* It is not small (an akerr_ErrorContext is tens of kilobytes), but the storage
* is allocated per thread only when that thread first touches the library's
* thread-local block, and the alternative is a shared buffer that two threads
* can be writing at once.
*/
static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ditch;
AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored;
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
akerr_ErrorLogFunction akerr_log_method = NULL;
/*
* One recursive lock over both the error pool and the status registry. See
* src/lock.h for why it is one lock, and why it is recursive.
*
* Everything below that touches either table does so through a function whose
* name ends in _locked, called from a wrapper that takes the lock and releases
* it on the single return path. The wrappers exist because the locked bodies
* are written with the FAIL_*_RETURN macros, which return from the middle of a
* function -- so those bodies cannot be the ones holding the lock.
*/
static akerr_Mutex akerr_state_lock;
/*
* Status-name registry.
*
@@ -97,6 +126,9 @@ akerr_ErrorContext *__akerr_copy_string(char *destination, int capacity,
* A range test also accepts pointers into the *interior* of an element, which
* would then be treated as the head of an akerr_ErrorContext and written
* through. Keep this an element-wise scan; it is not a missed optimization.
*
* Takes no lock: the addresses of the pool slots are fixed for the life of the
* process, and nothing here reads a slot's contents.
*/
int akerr_valid_error_address(akerr_ErrorContext *ptr)
{
@@ -156,77 +188,121 @@ void __akerr_name_library_status(int status, const char *name)
}
/*
* Idempotent. `inited` is set before any work so that the registry calls below
* -- and the public registry entry points, which all call akerr_init() so that
* a consumer reserving its range before anything else touches the library
* cannot have that reservation wiped by a later first-use of the pool -- see
* themselves as already initialized instead of recursing.
* The calling thread's last-ditch context.
*
* Thread-local storage starts zeroed, so a NULL stack-trace cursor means this
* thread has not used its copy yet. Checking the cursor rather than a separate
* flag keeps the whole thing self-describing: the cursor is the one field that
* must not be zero for the context to be usable at all.
*/
static akerr_ErrorContext *akerr_last_ditch_context(void)
{
if ( __akerr_last_ditch.stacktracebufptr == NULL ) {
memset((void *)&__akerr_last_ditch, 0x00, sizeof(akerr_ErrorContext));
__akerr_last_ditch.stacktracebufptr = (char *)&__akerr_last_ditch.stacktracebuf;
}
return &__akerr_last_ditch;
}
static AKERR_THREAD_LOCAL int akerr_initializing;
static akerr_Once akerr_state_once = AKERR_ONCE_INIT;
/*
* Runs exactly once per process, under akerr_once(). Everything it calls --
* the registry entry points, and the pool underneath them -- calls akerr_init()
* itself, so the first thing it does is raise the re-entry flag; see
* akerr_init() below.
*
* The lock is initialized before anything that could take it. That ordering is
* the reason this work lives in a once-routine instead of a
* check-a-flag-and-go: a second thread arriving while this one is still
* populating the tables must block until they are complete, not walk them.
*/
static void akerr_init_state(void)
{
akerr_mutex_init(&akerr_state_lock);
akerr_initializing = 1;
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
memset((void *)&AKERR_ARRAY_ERROR[i], 0x00, sizeof(akerr_ErrorContext));
AKERR_ARRAY_ERROR[i].arrayid = i;
AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf;
}
__akerr_last_ignored = NULL;
(void)akerr_last_ditch_context();
if ( akerr_log_method == NULL ) {
akerr_log_method = &akerr_default_logger;
}
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
memset((void *)&akerr_status_names[0], 0x00, sizeof(akerr_status_names));
memset((void *)&akerr_status_ranges[0], 0x00, sizeof(akerr_status_ranges));
akerr_status_name_count = 0;
akerr_status_range_count = 0;
/* errno and AKERR_* values are the library-owned compatibility band.
* This must precede every registration below: naming a status is only
* permitted inside a reserved range. Terminal for the same reason as
* __akerr_name_library_status(), and handled the same way: without this
* band the library owns nothing, so none of the names below could
* register either. */
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
AKERR_LIBRARY_OWNER));
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
/* Every AKERR_* code gets a name; tests/err_error_names.c asserts the
* list is exhaustive so a new code cannot be added without one. */
__akerr_name_library_status(AKERR_NULLPOINTER, "Null Pointer Error");
__akerr_name_library_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
__akerr_name_library_status(AKERR_API, "API Error");
__akerr_name_library_status(AKERR_ATTRIBUTE, "Attribute Error");
__akerr_name_library_status(AKERR_TYPE, "Type Error");
__akerr_name_library_status(AKERR_KEY, "Key Error");
__akerr_name_library_status(AKERR_INDEX, "Index Error");
__akerr_name_library_status(AKERR_FORMAT, "Format Error");
__akerr_name_library_status(AKERR_IO, "Input Output Error");
__akerr_name_library_status(AKERR_VALUE, "Value Error");
__akerr_name_library_status(AKERR_RELATIONSHIP, "Relationship Error");
__akerr_name_library_status(AKERR_EOF, "End Of File");
__akerr_name_library_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
__akerr_name_library_status(AKERR_ITERATOR_BREAK, "Iterator Break");
__akerr_name_library_status(AKERR_NOT_IMPLEMENTED, "Not Implemented");
__akerr_name_library_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
__akerr_name_library_status(AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap");
__akerr_name_library_status(AKERR_STATUS_RANGE_FULL, "Status Range Table Full");
__akerr_name_library_status(AKERR_STATUS_RANGE_INVALID, "Invalid Status Range");
__akerr_name_library_status(AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name");
__akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name");
__akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full");
__akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name");
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
akerr_init_errno();
#endif
akerr_initializing = 0;
}
/*
* Idempotent, and safe to call from any thread at any time. Every public entry
* point calls it -- so that a consumer reserving its range before anything else
* touches the library cannot have that reservation wiped by a later first-use
* of the pool -- which means it is also on the path of everything
* akerr_init_state() itself calls.
*
* The re-entry guard is thread local, and has to be: only the thread running
* the once-routine may skip past it. A second thread arriving mid-initialization
* must block inside akerr_once() until the tables are complete, which a shared
* flag set at the top of initialization would have let it walk right past.
*/
void akerr_init()
{
static int inited = 0;
if ( inited == 0 ) {
inited = 1;
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
memset((void *)&AKERR_ARRAY_ERROR[i], 0x00, sizeof(akerr_ErrorContext));
AKERR_ARRAY_ERROR[i].arrayid = i;
AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf;
}
__akerr_last_ignored = NULL;
memset((void *)&__akerr_last_ditch, 0x00, sizeof(akerr_ErrorContext));
__akerr_last_ditch.stacktracebufptr = (char *)&__akerr_last_ditch.stacktracebuf;
if ( akerr_log_method == NULL ) {
akerr_log_method = &akerr_default_logger;
}
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
memset((void *)&akerr_status_names[0], 0x00, sizeof(akerr_status_names));
memset((void *)&akerr_status_ranges[0], 0x00, sizeof(akerr_status_ranges));
akerr_status_name_count = 0;
akerr_status_range_count = 0;
/* errno and AKERR_* values are the library-owned compatibility band.
* This must precede every registration below: naming a status is only
* permitted inside a reserved range. Terminal for the same reason as
* __akerr_name_library_status(), and handled the same way: without this
* band the library owns nothing, so none of the names below could
* register either. */
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
AKERR_LIBRARY_OWNER));
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
/* Every AKERR_* code gets a name; tests/err_error_names.c asserts the
* list is exhaustive so a new code cannot be added without one. */
__akerr_name_library_status(AKERR_NULLPOINTER, "Null Pointer Error");
__akerr_name_library_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
__akerr_name_library_status(AKERR_API, "API Error");
__akerr_name_library_status(AKERR_ATTRIBUTE, "Attribute Error");
__akerr_name_library_status(AKERR_TYPE, "Type Error");
__akerr_name_library_status(AKERR_KEY, "Key Error");
__akerr_name_library_status(AKERR_INDEX, "Index Error");
__akerr_name_library_status(AKERR_FORMAT, "Format Error");
__akerr_name_library_status(AKERR_IO, "Input Output Error");
__akerr_name_library_status(AKERR_VALUE, "Value Error");
__akerr_name_library_status(AKERR_RELATIONSHIP, "Relationship Error");
__akerr_name_library_status(AKERR_EOF, "End Of File");
__akerr_name_library_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
__akerr_name_library_status(AKERR_ITERATOR_BREAK, "Iterator Break");
__akerr_name_library_status(AKERR_NOT_IMPLEMENTED, "Not Implemented");
__akerr_name_library_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
__akerr_name_library_status(AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap");
__akerr_name_library_status(AKERR_STATUS_RANGE_FULL, "Status Range Table Full");
__akerr_name_library_status(AKERR_STATUS_RANGE_INVALID, "Invalid Status Range");
__akerr_name_library_status(AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name");
__akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name");
__akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full");
__akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name");
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
akerr_init_errno();
#endif
if ( akerr_initializing != 0 ) {
return;
}
akerr_once(&akerr_state_once, &akerr_init_state);
}
void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
@@ -237,23 +313,46 @@ void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
exit(errctx->status);
}
/*
* Claim the lowest free slot. Finding it and taking the reference are one
* operation under the lock: a scan that returned an unclaimed slot would hand
* the same one to every thread that scanned before the first of them got around
* to incrementing the count.
*/
akerr_ErrorContext *akerr_next_error()
{
akerr_ErrorContext *found = (akerr_ErrorContext *)NULL;
akerr_init();
akerr_mutex_lock(&akerr_state_lock);
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
if ( AKERR_ARRAY_ERROR[i].refcount == 0 ) {
return &AKERR_ARRAY_ERROR[i];
found = &AKERR_ARRAY_ERROR[i];
found->refcount = 1;
break;
}
}
return (akerr_ErrorContext *)NULL;
akerr_mutex_unlock(&akerr_state_lock);
return found;
}
/*
* The wipe returns the slot to the pool, so it and the decrement that triggers
* it are one operation under the lock. Otherwise a thread that saw the count
* reach zero could be handed the slot by akerr_next_error() and start writing
* its error into it while the releasing thread was still memsetting it.
*/
akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
{
int oldid = 0;
akerr_ErrorContext *remaining = err;
akerr_init();
if ( err == NULL ) {
akerr_ErrorContext *errctx = &__akerr_last_ditch;
akerr_ErrorContext *errctx = akerr_last_ditch_context();
FAIL_RETURN(errctx, AKERR_NULLPOINTER, "akerr_release_error got NULL context pointer");
}
akerr_mutex_lock(&akerr_state_lock);
if ( err->refcount > 0 ) {
err->refcount -= 1;
}
@@ -262,9 +361,10 @@ akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
memset(err, 0x00, sizeof(akerr_ErrorContext));
err->stacktracebufptr = (char *)&err->stacktracebuf;
err->arrayid = oldid;
return NULL;
remaining = NULL;
}
return err;
akerr_mutex_unlock(&akerr_state_lock);
return remaining;
}
@@ -288,7 +388,7 @@ static unsigned akerr_status_hash(int status)
/*
* Find the slot holding `status`. With create != 0, claim a free slot for it if
* it is not present yet. Returns NULL when the status is absent and either no
* slot was requested or the registry is full.
* slot was requested or the registry is full. Caller holds akerr_state_lock.
*
* The `& (AKERR_STATUS_NAME_SLOTS - 1)` below is load-bearing and fails
* silently: off by one in either direction and the probe indexes past
@@ -328,7 +428,8 @@ static akerr_StatusName *akerr_status_slot(int status, int create)
return NULL;
}
/* The reservation covering `status`, or NULL if nobody has claimed it. */
/* The reservation covering `status`, or NULL if nobody has claimed it. Caller
* holds akerr_state_lock. */
static akerr_StatusRange *akerr_range_for_status(int status)
{
for ( int i = 0; i < akerr_status_range_count; i++ ) {
@@ -348,10 +449,14 @@ static akerr_StatusRange *akerr_range_for_status(int status)
* fails to register degrades into "Unknown Error" in stack traces, which is
* exactly the kind of quiet loss this registry exists to prevent -- so the
* message carries everything a caller needs to see in a stack trace.
*
* Caller holds akerr_state_lock. The FAIL_* macros below re-enter the library
* to build their error -- a pool slot from akerr_next_error(), and a status
* name for the stack trace -- and that re-entry is why the lock is recursive.
*/
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *owner,
int status,
const char *name)
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name_locked(const char *owner,
int status,
const char *name)
{
akerr_StatusRange *range;
akerr_StatusName *entry;
@@ -394,12 +499,16 @@ static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *ow
/*
* Register a name for a status inside a range the caller reserved. Both strings
* are checked here rather than only inside akerr_store_status_name(): the store
* accepts a NULL owner for the legacy akerr_name_for_status() path, so a NULL
* arriving through *this* entry point would be read as "caller did not identify
* itself" and skip the ownership check entirely.
* are checked here rather than only inside akerr_store_status_name_locked(): the
* store accepts a NULL owner for the legacy akerr_name_for_status() path, so a
* NULL arriving through *this* entry point would be read as "caller did not
* identify itself" and skip the ownership check entirely.
*
* Caller holds akerr_state_lock.
*/
akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name)
static akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name_locked(const char *owner,
int status,
const char *name)
{
PREPARE_ERROR(errctx);
@@ -410,10 +519,21 @@ akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, co
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
"Refusing to name status %d for %s: the name is NULL",
status, owner);
PASS(errctx, akerr_store_status_name(owner, status, name));
PASS(errctx, akerr_store_status_name_locked(owner, status, name));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name)
{
akerr_ErrorContext *errctx;
akerr_init();
akerr_mutex_lock(&akerr_state_lock);
errctx = akerr_register_status_name_locked(owner, status, name);
akerr_mutex_unlock(&akerr_state_lock);
return errctx;
}
/*
* Return or set a name. Status magnitude is unrelated to storage size.
*
@@ -426,16 +546,40 @@ akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, co
* The lookup path (name == NULL) deliberately stays clear of all of this. FAIL
* calls it to render a status into a stack trace, so it must not itself need an
* error context.
*
* The name is returned by pointer into the registry, which never resizes and
* never removes an entry, so the pointer is good for the life of the process.
* Its *contents* are stable as long as nobody registers a second name for the
* same status: a rename overwrites the buffer in place, and a lookup on another
* thread can be reading it. Register names during initialization -- renaming a
* live status while other threads run is the one registry operation the lock
* cannot make safe, because the reader is outside it by then.
*/
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *owner,
int status,
const char *name)
{
akerr_ErrorContext *errctx;
akerr_mutex_lock(&akerr_state_lock);
errctx = akerr_store_status_name_locked(owner, status, name);
akerr_mutex_unlock(&akerr_state_lock);
return errctx;
}
char *akerr_name_for_status(int status, char *name)
{
akerr_StatusName *entry;
char *found = "Unknown Error";
akerr_init();
if ( name != NULL ) {
PREPARE_ERROR(errctx);
int refused = 0;
/* The store takes and releases the lock itself, so the handler below
* calls akerr_log_method -- consumer code, which may do anything at all
* including calling back into this library -- without holding it. */
ATTEMPT {
CATCH(errctx, akerr_store_status_name(NULL, status, name));
} CLEANUP {
@@ -449,15 +593,22 @@ char *akerr_name_for_status(int status, char *name)
return "Unknown Error";
}
}
akerr_mutex_lock(&akerr_state_lock);
entry = akerr_status_slot(status, 0);
if ( entry == NULL ) {
return "Unknown Error";
if ( entry != NULL ) {
found = entry->name;
}
return entry->name;
akerr_mutex_unlock(&akerr_state_lock);
return found;
}
/* Reserve an inclusive status interval and reject collisions. */
akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner)
/* Reserve an inclusive status interval and reject collisions. Caller holds
* akerr_state_lock: the overlap scan and the entry that follows it are one
* decision, so two threads claiming overlapping ranges at once must not be able
* to both find the table clear. */
static akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range_locked(int first_status,
int count,
const char *owner)
{
int last_status;
PREPARE_ERROR(errctx);
@@ -510,3 +661,14 @@ akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, cons
akerr_status_range_count++;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner)
{
akerr_ErrorContext *errctx;
akerr_init();
akerr_mutex_lock(&akerr_state_lock);
errctx = akerr_reserve_status_range_locked(first_status, count, owner);
akerr_mutex_unlock(&akerr_state_lock);
return errctx;
}

121
src/lock.h Normal file
View File

@@ -0,0 +1,121 @@
#ifndef _AKERR_LOCK_H_
#define _AKERR_LOCK_H_
/*
* Serialization for the library's process-global state: the error pool
* (AKERR_ARRAY_ERROR) and the status registry. Private to the library -- none
* of this appears in the installed header, so the backend is not part of the
* ABI and can be changed without touching a consumer.
*
* The backend is chosen at configure time by the AKERR_THREADS build option and
* never by autodetection here. A build that quietly decided it did not need
* locking is exactly the failure this has to prevent: it would produce a
* library that reports itself thread safe and is not.
*
* AKERR_THREADS_PTHREAD POSIX threads.
* AKERR_THREADS_NONE No locking at all, for a build that has declared
* itself single threaded (-DAKERR_THREADS=none).
*
* One lock covers both tables, and it is recursive. Both are deliberate:
*
* - Raising an error re-enters the library. FAIL() calls
* akerr_name_for_status() to render the status into the stack trace and
* ENSURE_ERROR_READY() to check a context out of the pool, so a refusal
* raised from inside a locked registry operation takes the lock again on
* the same thread. A non-recursive mutex deadlocks there.
* - With a single lock there is no lock ordering to get wrong, and no way for
* a future caller to acquire the pool and the registry in the opposite
* order from this file.
*
* The cost is that error *construction* is serialized across threads. Errors
* are the exceptional path; correctness is worth more there than throughput.
*/
/*
* PTHREAD_MUTEX_RECURSIVE is XSI, so glibc hides it under a strict -std=c99
* without _XOPEN_SOURCE. No feature-test macro is defined here, because the
* public header already needs the same one for PATH_MAX: a build strict enough
* to lose one has already lost the other. Build with -D_XOPEN_SOURCE=700 if you
* need strict C99.
*/
#if defined(AKERR_THREADS_PTHREAD) && AKERR_THREADS_PTHREAD == 1
#include <pthread.h>
#include <stdlib.h>
typedef pthread_mutex_t akerr_Mutex;
typedef pthread_once_t akerr_Once;
#define AKERR_ONCE_INIT PTHREAD_ONCE_INIT
/*
* Terminal on failure. There is no error context to raise into: the pool one
* would come from is the thing this lock protects, and every path that could
* report the failure needs the lock to do it. A process whose error library
* silently stopped locking is worse than one that stops here.
*/
static void akerr_mutex_init(akerr_Mutex *mutex)
{
pthread_mutexattr_t attr;
if ( pthread_mutexattr_init(&attr) != 0 ||
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) != 0 ||
pthread_mutex_init(mutex, &attr) != 0 ) {
abort();
}
pthread_mutexattr_destroy(&attr);
}
static void akerr_mutex_lock(akerr_Mutex *mutex)
{
pthread_mutex_lock(mutex);
}
static void akerr_mutex_unlock(akerr_Mutex *mutex)
{
pthread_mutex_unlock(mutex);
}
static void akerr_once(akerr_Once *once, void (*routine)(void))
{
pthread_once(once, routine);
}
#elif defined(AKERR_THREADS_NONE) && AKERR_THREADS_NONE == 1
typedef char akerr_Mutex;
typedef int akerr_Once;
#define AKERR_ONCE_INIT 0
static void akerr_mutex_init(akerr_Mutex *mutex)
{
(void)mutex;
}
static void akerr_mutex_lock(akerr_Mutex *mutex)
{
(void)mutex;
}
static void akerr_mutex_unlock(akerr_Mutex *mutex)
{
(void)mutex;
}
/*
* The flag is raised before the routine runs, so a routine that calls back into
* akerr_init() sees initialization already in progress and does not recurse --
* the same short-circuit the pthread backend gets from akerr_initializing.
*/
static void akerr_once(akerr_Once *once, void (*routine)(void))
{
if ( *once == 0 ) {
*once = 1;
routine();
}
}
#else
#error "No threading backend selected. Build libakerror through its CMake, which defines AKERR_THREADS_PTHREAD or AKERR_THREADS_NONE from the AKERR_THREADS option."
#endif
#endif // _AKERR_LOCK_H_