Spec couche 1 — air-thread (threading et synchronisation)
Spécification technique — Version 1.0. Couche 1 « Primitives système ».
Renvoi ADR-055 (no_std + spawn gardé per-cible). Depuis ADR-055 D3,
air-threadest#![no_std]: la surface livrée pour la cible*-linux-air(AirMutex/AirRwLock/AirSemaphore/AirChannelsurfutex(2),runtime_primitives) est no_std pure. Le spawn ergonomiqueAirThreadBuilder/AirThreadHandle(thread.rs, enrobantstd::thread) est gardé#[cfg(not(target_env = "air"))]: disponible sur l’hôte (gnu, sound sous glibc), non compilé pour la cible — le spawn Air-natif (closure sur TCB Air) relève d’air-runtime+ allocateur réel (chantier distinct, ADR-052 D7.3). Les mentions « s’appuie surstd» ci-dessous valent donc pour l’hôte.
Position et méthode
air-thread fournit les primitives de threading (AirThread), de
synchronisation (AirMutex/AirRwLock/AirSemaphore), les channels MPSC
(AirChannel) et un ré-export d’atomics. S’appuie sur air-base-lib
(AirError). API Rust d’abord (ABI C différée). Méthode doc-d’abord.
Pas d’async runtime ici — l’asynchrone (event loop, tâches) est le runtime Air de la couche 2 (ADR-023 : runtime maison sur io_uring, pas Tokio).
air-thread= primitives synchrones de threads système et de synchronisation bloquante.
air-threads’appuie surstd— c’est légitime. La règle « pas de libc / syscalls directs » vaut pour la couche 0. Les couches ≥ 1 utilisentstdnormalement. Réimplémenter les threads surclone3(TLS, stacks, machinerie pthread) serait déraisonnable et risqué :AirThreadenrobestd::thread; les locks enrobentstd::sync. Conséquence : très peu d’unsafeinterne dans cette crate (contraste avecair-memory).
Décision : backend std::sync pour la synchro
Depuis Rust 1.62,
std::sync::Mutex/RwLocksur Linux sont des locks basés sur le futex kernel (chemin rapide en quelques atomiques, syscall futex en contention) — essentiellement ce qu’on écrirait à la main. On les enrobe : Air possède le type (pour l’ABI C et un nommage uniforme) ;stdfournit le backend prouvé. Zéro dépendance externe, zéro manque couche 0. Écarté :parking_lot(dépendance, à contre-courant de la philo minimiser-les-deps qui a écarté Tokio).
stdest un expédient de v1 — le cap est une implémentation futex maison (à graver). Adopterstd::syncpermet de livrer maintenant sans risque, mais ce n’est pas l’état cible. Avecstd, on hérite des contraintes de la bibliothèque standard de Rust — portabilité multi-plateforme, sémantique de poisoning, choix d’équité, garanties de panic-safety — qui ne sont pas forcément les nôtres. Notre propre implémentation, sur un futex synchrone couche 0 (à produire), nous fait maîtriser exactement le comportement (équité, file d’attente, sémantique sur panique, budgets) — cohérent avec la raison d’être du projet (contrôle vertical du stack, build-own). Ce remplacement est planifié (cf. travail à reprendre), pas un simple plan B de performance. La frontière d’APIair-threadest conçue pour que ce basculementstd→ futex-maison soit interne (l’API publique ne change pas).(Réconciliation passe-2 : la macro-archi disait « au-dessus de parking_lot ».)
Dépendances (règle des 80 %, ADR-024)
air-base-lib(erreurs) +std. Aucune dépendance externe en v1 (niparking_lot, nicrossbeam).
Section 1 — Threads : AirThread
#![allow(unused)]
fn main() {
/// Constructeur de thread (enrobe `std::thread::Builder`).
pub struct AirThreadBuilder { /* name, stack_size, affinity */ }
impl AirThreadBuilder {
pub fn new() -> Self;
pub fn name(self, name: &str) -> Self; // posé via PR_SET_NAME (std / couche 0)
pub fn stack_size(self, bytes: usize) -> Self;
/// Affinité CPU souhaitée (cf. **manque couche 0** ci-dessous).
pub fn cpu_affinity(self, cpus: &[u32]) -> Self;
/// Lance le thread. Le corps retourne `T` ; `AirThreadHandle::join` le récupère.
/// # Errors `AirError` si la création échoue (ressources).
pub fn spawn<F, T>(self, body: F) -> AirResult<AirThreadHandle<T>>
where F: FnOnce() -> T + Send + 'static, T: Send + 'static;
}
/// Handle joignable d'un thread.
pub struct AirThreadHandle<T> { /* std::thread::JoinHandle<T> */ }
impl<T> AirThreadHandle<T> {
/// Attend la fin et récupère la valeur.
/// # Errors `AirError` (catégorie `Other`) si le thread a **paniqué** (la
/// panique est capturée à la frontière, pas propagée silencieusement).
pub fn join(self) -> AirResult<T>;
pub fn is_finished(&self) -> bool;
}
/// Nom du thread courant (le cas échéant).
pub fn current_thread_name() -> Option<String>;
}
⚠️ Manque couche 0 :
sched_setaffinity/sched_getaffinity(absents).cpu_affinityexigesched_setaffinity— non wrappé en couche 0. L’API est spécifiée ici, mais son implémentation est différée à une petite extension couche 0sched(coordonnée), commefs::inotify/epoll/privsep. Sans elle,cpu_affinityrenvoieUnsupported(ou la méthode est#[cfg]-gated).nameetstack_sizefonctionnent dès v1 (viastd/set_thread_nameprésent).
Section 2 — Synchronisation
#![allow(unused)]
fn main() {
/// Mutex Air (sur `std::sync::Mutex`), **sans poisoning** : une panique en section
/// critique **n'empoisonne pas** le mutex — `lock` rend toujours un guard, jamais
/// une erreur de poison. (La panique est récupérée à la frontière du thread, via
/// `join`.) Trade-off documenté : si une panique laisse la donnée incohérente,
/// les verrouilleurs suivants la voient telle quelle — à l'appelant de rétablir
/// l'invariant explicitement s'il y tient.
pub struct AirMutex<T> { /* std::sync::Mutex<T> */ }
impl<T> AirMutex<T> {
pub fn new(value: T) -> Self;
pub fn lock(&self) -> AirMutexGuard<'_, T>; // pas de Result (no poisoning)
pub fn try_lock(&self) -> Option<AirMutexGuard<'_, T>>;
pub fn get_mut(&mut self) -> &mut T; // accès exclusif sans lock
pub fn into_inner(self) -> T;
}
pub struct AirMutexGuard<'a, T> { /* Deref / DerefMut */ }
/// RwLock Air (sur `std::sync::RwLock`), **sans poisoning**.
pub struct AirRwLock<T> { /* std::sync::RwLock<T> */ }
impl<T> AirRwLock<T> {
pub fn new(value: T) -> Self;
pub fn read(&self) -> AirRwLockReadGuard<'_, T>; // N lecteurs simultanés
pub fn write(&self) -> AirRwLockWriteGuard<'_, T>; // 1 écrivain exclusif
pub fn try_read(&self) -> Option<AirRwLockReadGuard<'_, T>>;
pub fn try_write(&self) -> Option<AirRwLockWriteGuard<'_, T>>;
}
/// Sémaphore comptant (sur `Mutex<usize>` + `Condvar` — `std` n'en fournit pas).
pub struct AirSemaphore { /* Mutex + Condvar */ }
impl AirSemaphore {
pub fn new(permits: usize) -> Self;
/// Acquiert un permis (bloque si 0). Le permis est rendu au `Drop` du guard.
pub fn acquire(&self) -> AirSemaphorePermit<'_>;
pub fn try_acquire(&self) -> Option<AirSemaphorePermit<'_>>;
pub fn available_permits(&self) -> usize;
}
pub struct AirSemaphorePermit<'a> { /* rend le permis au Drop */ }
}
Décision (no poisoning). Le poisoning de std (un Mutex devient Err après
un panic d’un détenteur) est plus souvent une nuisance qu’une aide (c’est la raison
n°1 d’adopter parking_lot). air-thread l’absorbe (unwrap_or_else(|e| e.into_inner()) en interne) : l’API ne rend jamais d’erreur de poison. Cohérent
avec la gestion de panique à la frontière du thread (join rapporte la
panique).
Section 3 — Channels MPSC : AirChannel
#![allow(unused)]
fn main() {
/// Canal **multi-producteurs / un consommateur** (sur `std::sync::mpsc`).
pub fn air_channel<T: Send>() -> (AirSender<T>, AirReceiver<T>);
pub struct AirSender<T> { /* Clone — chaque clone est un producteur */ }
pub struct AirReceiver<T> { /* unique consommateur */ }
impl<T: Send> AirSender<T> {
/// # Errors `AirError` (`BrokenResource`) si le récepteur a été abandonné.
pub fn send(&self, value: T) -> AirResult<()>;
}
impl<T: Send> AirReceiver<T> {
/// Bloque jusqu'à un message. # Errors `BrokenResource` si **tous** les
/// senders sont abandonnés (canal vide et fermé).
pub fn recv(&self) -> AirResult<T>;
/// `Ok(None)` si vide, `Err(BrokenResource)` si fermé.
pub fn try_recv(&self) -> AirResult<Option<T>>;
/// `Ok(None)` si le délai expire avant un message.
pub fn recv_timeout(&self, timeout: AirDuration) -> AirResult<Option<T>>;
}
}
Section 4 — Atomics
Ré-export des types atomiques de std sous air_thread::atomic
(AtomicU32/AtomicUsize/AtomicBool/… + Ordering) — pour une surface Air
uniforme (macro-architecture), sans réécriture (les atomiques de core sont la
référence kernel/ISA). Un développeur qui connaît std les retrouve à l’identique.
Récapitulatif air-thread
| Domaine | API | Backend |
|---|---|---|
| Threads | AirThreadBuilder, AirThreadHandle, current_thread_name | std::thread (+ set_thread_name) |
| Synchro | AirMutex, AirRwLock, AirSemaphore (+ guards) | std::sync (no poisoning) ; sémaphore = Mutex+Condvar |
| Channels | air_channel, AirSender, AirReceiver | std::sync::mpsc |
| Atomics | ré-export air_thread::atomic | core::sync::atomic |
Différé : cpu_affinity (dépend de l’ajout couche 0 sched_setaffinity) ;
variantes/parités async (couche 2) ; ABI C.
Stratégie de tests
- Couverture 100 % lignes + branches (Principe 1).
- Concurrence :
AirMutex— incréments concurrents de N threads → total exact ; no-poisoning : un panic en section critique laisse le mutex utilisable ;AirRwLock— N lecteurs simultanés, écrivain exclusif ;AirSemaphore— N permis,acquirebloque à 0 puis débloqué par un release ;AirChannel— MPSC (plusieurs senders, un receiver),recvbloque, sender abandonné →recverreur,recv_timeout. - loom sur la seule structure concurrente maison :
AirSemaphore(Mutex+Condvar),std↔loomcommuté souscfg(loom)(même schéma que le slab io_uring). Les wrappersMutex/RwLock(surstd, déjà prouvés) n’en ont pas besoin. - Miri (avec préemption) sur les chemins concurrents courts ; threads/join.
- Threads : valeur retournée par
join; panic →joinrend une erreur (capturée, non propagée). - Doctests : exemples compilent et passent.
- (Pas de fuzzing : aucune ingestion de données externes.)
Décisions de fond
std::thread/std::synccomme backend de v1 — cap acté vers futex maison. Air possède les types (ABI C, nommage) ;stdfournit le futex-based prouvé, zéro dep, zéro manque couche 0. Maisstdporte les contraintes de la std de Rust (pas les nôtres) → remplacement planifié par une implémentation futex synchrone maison (contrôle total du comportement), basculement interne (API inchangée). Voir travail à reprendre.- No poisoning — l’API ne rend jamais d’erreur de poison ; panique gérée à la
frontière du thread (
join). Trade-off documenté. - Pas d’async ici (ADR-023 ; couche 2).
AirThreadenrobestd::thread(pas de réimplémentation surclone3).- Ré-export d’atomics plutôt que réécriture (autorité
core). cpu_affinityimplémenté (PR #97) — via la couche 0set_cpu_affinity(PR #55) ; handshake par canaux (l’enfant publie sonTid, l’appelant pose l’affinité avant de débloquer le corps ; un masque refusé fait échouerspawnavant exécution du corps).
Travail à reprendre
- Extension couche 0
sched:sched_setaffinity/sched_getaffinity(prérequis decpu_affinity). 5ᵉ manque couche 0, non urgent. - Build-natif futex des locks (
AirMutex/AirRwLock/AirSemaphore) — PLANIFIÉ, pas conditionnel. Raison de fond : maîtriser le comportement (équité, file, sémantique sur panique, budgets) plutôt que d’hériter des contraintes de la std de Rust ; la performance (Principe 5) n’est qu’un bonus. Prérequis : une primitive couche 0 futex synchrone (FUTEX_WAIT/WAKEbloquants — 6ᵉ manque couche 0 à produire ; distincte du futex io_uring async déjà livré). Locks à vérifier loom. Le basculement est interne (APIair-threadinchangée). - ABI C d’
air-thread; autres crates couche 1 (air-socket,air-crypto,air-device).
Licence du document : MPL 2.0
Statut : Spécification technique d’air-thread (couche 1). API Rust ; ABI C
différée. Backend std ; synchro sans poisoning ; pas d’async (couche 2).