> ## Documentation Index
> Fetch the complete documentation index at: https://zinc.ossl.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Notify and Wait

> Cross-process synchronization with the notify/wait mechanism

Zinc provides a lightweight signal mechanism for cross-process synchronization. It is not a mutex or condition variable. It is a one-shot "data changed" signal.

## How it works

The header of every shared region contains a `notify_seq` field: an atomic 32-bit counter. The notify/wait API builds on this single counter:

1. The writer calls `notify()`, which atomically increments `notify_seq` and wakes any waiting threads.
2. The reader calls `wait()`, which blocks until `notify_seq` changes from the value it last observed.

This design avoids a common race condition. If the writer increments the sequence number before the reader starts waiting, and the reader waits for the *current* value, it blocks forever — the counter already changed. Zinc's `wait()` tracks a per-handle `last_seq` value instead. It waits for the counter to change *from the last value it saw*, not from whatever it currently reads.

On Linux, `wait()` uses the `futex` syscall. It parks the calling thread in the kernel until the futex address changes or the timeout expires. Zero overhead with no waiters: `notify()` still does an atomic increment and a `FUTEX_WAKE`, but with no waiters the wake is a no-op.

On macOS and other non-Linux platforms, the wait uses an adaptive spin loop with exponential backoff. It spins for a brief period, then yields the thread, periodically checking for the timeout.

## Usage pattern

The typical pattern is a producer-consumer arrangement:

```rust theme={null}
// Producer process
let region = SharedRegion::create("stream", 65536)?;

loop {
    // Write new data
    write_frame(region.as_ptr());

    // Signal consumers
    region.notify();
}
```

```rust theme={null}
// Consumer process
let region = SharedRegion::open("stream")?;

loop {
    // Block until new data is available
    region.wait(u32::MAX); // effectively infinite

    // Read the latest data
    process_frame(region.as_ptr());
}
```

## Timeouts

`wait()` accepts a timeout in milliseconds. If no notification arrives within the timeout, the call returns `Err(TimedOut)` (or `false` / an exception in language adapters that wrap the error).

A timeout of `u32::MAX` (4294967295) is effectively infinite. On Linux, the futex `timespec` for this value is about 49.7 days. For practical purposes, it will not expire.

```rust theme={null}
// Wait up to 100 milliseconds
match region.wait(100) {
    Ok(()) => println!("data available"),
    Err(ZincError::TimedOut) => println!("no update"),
    Err(e) => println!("error: {e}"),
}
```

## Multiple waiters

`notify()` wakes all waiters simultaneously. Each waiter that returns from `wait()` reads the current `notify_seq` into its `last_seq` and returns. All waiters see the same counter value, which means they all see the same generation of data.

This is by design: if you have one writer and multiple readers, all readers are notified of every update. Each reader independently reads the shared data. There is no need for coordinated wake order.

## Caveats

**Notify coalesces.** If the writer calls `notify()` twice before the reader calls `wait()`, the reader sees only one notification. The sequence number goes from N to N+2. The reader's `wait()` unblocks because N+2 != last\_seq. But the reader does not know there were two updates. That works for state-based sync (the consumer always reads the latest data), not event-based sync (where every event must be processed).

**Notify and wait are not a mutex.** Writing to shared memory while another thread is reading can produce torn reads if the data is larger than the native word size. If you need atomic multi-field updates, layer your own synchronization on top. A simple approach: use the first 8 bytes of the data area as a generation counter, increment it at the start of a write and again at the end. Readers can then detect in-progress writes by checking whether the two counters match.

**Wait is not re-entrant.** Calling `wait()` recursively from different threads is not tested and not guaranteed to work correctly. Each handle should be used from at most one waiting thread at a time.

## Linux futex details

On Linux, the notify/wait path goes through the `futex` syscall:

* `notify()` calls `FUTEX_WAKE` with `wake_count = i32::MAX` to wake all waiters.
* `wait()` calls `FUTEX_WAIT` with a `timespec` derived from the millisecond timeout.

The `futex` syscall is well understood. Waking no waiters costs a single syscall. Waking one waiter costs two (one in the waker, one returning in the waiter after the kernel unparks it). Roundtrip latency is typically under 1 microsecond on modern hardware.

## macOS and other platforms

On macOS, `notify()` increments the sequence number atomically but does not perform a kernel-assisted wake. The `wait()` function spins in user space with a yield after 256 iterations. This is less efficient than futex but avoids the complexity of Mach semaphores or the private `ulock` API. Future versions may add a `ulock` backend for macOS.
