Concurrency model
The execution order, fairness, and reentrancy detection rules guaranteed by asyncmux, and how the detection scope differs by runtime.
Lock types
asyncmux handles two kinds of locks.
- Write lock (W) — An exclusive lock. It conflicts with locks on the same target (the same key in the
Asyncmuxclass). - Read lock (R) — A shared lock. It can coexist with read locks on the same target (the same key in the
Asyncmuxclass).
The decorator API and the function API hold a single lock state per target object. The Asyncmux class additionally holds per-key state, and keyless requests act as global locks that conflict with everything.
Ordering guarantees
Requests on the same target (the same key in the Asyncmux class) run in the following order.
| Request sequence | Execution order |
|---|---|
| W → W | Arrival order (FIFO) |
| W → R | R runs after W completes |
| R → W | W runs after all Rs are freed |
| R → R | Concurrent (order not guaranteed) |
While a write lock is waiting, a later-arriving read lock never overtakes it. Reads never starve a write, no matter how many arrive in a row.
In the Asyncmux class, non-conflicting requests (such as different keys) can overtake the wait queue and be acquired immediately. Overtaking only happens when there is no conflicting request, so per-key FIFO is preserved.
Reentrancy detection
While a decorator-locked method is running, the lock target and the held lock kind are recorded in the async context. Requesting a lock on the same target while holding it throws ReentrantLockError to prevent deadlocks.
| Held lock | Requested lock | Result |
|---|---|---|
| W | W | ReentrantLockError |
| W | R | ReentrantLockError |
| R | W | ReentrantLockError |
| R | R | Allowed (as a shared lock) |
Detection scope
- While a decorated method is running, reentrancy across
awaitis also detected (whenAsyncLocalStorageis available). - Circular waits across multiple objects are also detected.
class A {
@asyncmux
async m(b: B) {
await b.m(this);
}
}
class B {
@asyncmux
async m(a: A) {
await a.m(this); // Fails because it re-enters A while holding A's lock.
}
}
const b = new B();
await new A().m(b); // ReentrantLockError
- In environments without
AsyncLocalStorage, only the synchronous call scope is detected. Reentrancy acrossawaitis not detected. - The function API does not register a context, so it detects no reentrancy except while a decorated method is running.
- The
Asyncmuxclass does not detect reentrancy by default. With thepreventReentrancyoption, it detects reentrancy with the same conflict rules.
State cleanup
Lock state is freed once its target object or key is no longer needed.
- Decorator and function API state is managed with a
WeakMap. Once the target object is garbage-collected, its state is collected as well. When every lock is released and the wait queue is empty, the entry is removed. - Per-key counters in the
Asyncmuxclass are also removed once their hold count reaches 0.