Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Thread-safety by type

The C ABI declares, per type, what is callable from which thread (ADR-027 §B.4). In C the compiler checks nothing: honoring these policies is your responsibility. This page states them with the concrete pitfalls.

Policy table

Type / functionsPolicyWhat it allows
AirLog (air_log_emit, air_log_lost_count)ThreadSafesame handle shared across multiple threads
AirLog (air_log_close)consumesclose with no concurrent use
AirLogFields (air_log_fields_*)NOT thread-safebuild/mutate on one thread
Time & identifiers (air_instant_*, air_duration_*, air_uuid_*, air_id128_*, air_monotonic_id_next, air_status_message)reentrant / no shared statefree concurrent calls

AirLog = ThreadSafe

A single AirLog handle may be shared across multiple threads: its core is an Arc<JournalSink> and emission is an atomic datagram send. Multiple threads may therefore call air_log_emit / air_log_lost_count on the same handle, in parallel, with no locking on your part:

/* thread A and thread B, same `log` — OK */
air_log_emit(log, AIR_LOG_LEVEL_INFO,  "from A", NULL);
air_log_emit(log, AIR_LOG_LEVEL_ERROR, "from B", NULL);

Pitfall — closing, however, is not concurrent. air_log_close consumes the handle (it frees it). You must guarantee that no other thread is using log at the moment of close, and that it is called exactly once. Safe pattern: let all threads work, join them, then close from a single thread.

AirLogFields = NOT thread-safe

AirLogFields is a mutable builder: each air_log_fields_add[_bytes] modifies it. It is protected by no lock. Never touch it from two threads at once.

The intended — and safe — usage model is “build single-threaded, then read”:

/* 1. A SINGLE thread builds the builder. */
AirLogFields *f = NULL;
air_log_fields_new(&f);
air_log_fields_add(f, "REQUEST_ID", "abc-123");
air_log_fields_add(f, "USER",       "alice");

/* 2. Passed read-only (+0) to emit: safe as long as nobody mutates it in parallel. */
air_log_emit(log, AIR_LOG_LEVEL_INFO, "request handled", f);

air_log_fields_free(f);   /* freed by the owning thread */

The concrete danger. Two threads calling air_log_fields_add on the same AirLogFields = a data race (corruption of the internal structure, undefined behavior). If several threads must enrich a log, give each its own AirLogFields, or serialize the adds behind your mutex.

Why this choice? A builder is by nature a thread-local working object: making it thread-safe would impose a costly internal lock on everyone, for a rare need. The policy “one thread builds it, several can then log via a shared AirLog” covers the real case at no cost (Principle 5: don’t pay for what you don’t use).

Time & identifiers = reentrant / no shared state

All time functions (air_instant_now, air_instant_elapsed, air_instant_duration_since, air_duration_from_*, air_duration_as_secs_f64) and identifier functions (air_uuid_*, air_id128_*, air_status_message) are reentrant: they do only a kernel clock read and/or a computation on caller-local POD, with no mutable shared state. You may call them concurrently from as many threads as you like, each with its own out:

/* N threads, each with its own local AirInstant — no sharing, no lock */
AirInstant t;
air_instant_now(&t);

air_monotonic_id_next is the only one touching a process-global counter, but the increment is atomic: safe to call concurrently, each thread gets a distinct and strictly increasing value. (Monotonicity is guaranteed within a process; it is not an inter-process identifier.)

Mnemonic rule

  1. Logger (AirLog): shareable across threads to emit; close it alone, once, with no concurrent use.
  2. Fields (AirLogFields): one thread, one builder. Never mutate concurrently.
  3. Time & IDs: worry-free, everywhere, in parallel.

Version française : Thread-safety par type.