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 / functions | Policy | What it allows |
|---|---|---|
AirLog (air_log_emit, air_log_lost_count) | ThreadSafe | same handle shared across multiple threads |
AirLog (air_log_close) | consumes | close with no concurrent use |
AirLogFields (air_log_fields_*) | NOT thread-safe | build/mutate on one thread |
Time & identifiers (air_instant_*, air_duration_*, air_uuid_*, air_id128_*, air_monotonic_id_next, air_status_message) | reentrant / no shared state | free 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_closeconsumes the handle (it frees it). You must guarantee that no other thread is usinglogat the moment ofclose, and that it is called exactly once. Safe pattern: let all threads work, join them, thenclosefrom 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_addon the sameAirLogFields= a data race (corruption of the internal structure, undefined behavior). If several threads must enrich a log, give each its ownAirLogFields, or serialize theadds 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
- Logger (
AirLog): shareable across threads to emit; close it alone, once, with no concurrent use. - Fields (
AirLogFields): one thread, one builder. Never mutate concurrently. - Time & IDs: worry-free, everywhere, in parallel.
Version française : Thread-safety par type.