Zephyr Condition Variables: Couple Predicate Changes to Mutex Release and Recheck

Protect the shared predicate with a mutex, test it while holding that mutex, and wait in a loop that rechecks it after every return. A condition-variable signal tells a waiter to reconsider shared state. It does not reserve an item or prove that the state is still suitable when the waiter regains the mutex.

This guide uses Zephyr v3.7.0 condition variables and mutexes in thread context. It addresses the unlock/wait/reacquire protocol for competing consumers. It does not compare every notification primitive, and it does not treat task suspension or a state snapshot as a substitute for waiting correctly.

State the predicate independently of the signal

For a bounded work store, the predicate might be “at least one item is available, or shutdown has been requested.” The count, item links and shutdown state must have a defined synchronization owner. A signal is not the authoritative record that an item exists.

The Zephyr 3.7.0 condition-variable documentation describes an atomic release-and-wait operation followed by reacquiring the mutex before return. It also distinguishes the condition variable from the condition itself. Preserve that distinction in names and diagnostics: record predicate transitions separately from signal calls.

Use the same mutex for every read or mutation involved in the predicate. If the producer updates an item count outside that protection while the consumer relies on the mutex, the wait protocol cannot supply missing synchronization for those writes.

Condition-variable loop: lock the predicate mutex, test shared state, atomically release and wait when false, reacquire and recheck after wake, then claim work while the predicate remains protected.
The decision point is after mutex acquisition and after every wait return. A wakeup returns control to the predicate check, not directly to item consumption.

Make the consumer’s sequence explicit

  1. Acquire the predicate mutex and handle acquisition failure according to the caller’s contract.
  2. While no item is available and stopping is false, call k_condvar_wait() with that mutex and the chosen wait policy.
  3. On return, inspect the return code and shared predicate while the mutex is held again. Decide whether to keep waiting, accept available work or report timeout/shutdown.
  4. When the predicate permits consumption, remove or claim exactly the intended work while still holding the mutex.
  5. Release the mutex before doing unrelated or lengthy processing.

The producer uses the same mutex to publish a consistent item and update the predicate, signals or broadcasts according to the wake policy, and releases the mutex. Keep callbacks and formatting outside this protected sequence where possible so the operation’s correctness does not depend on hidden reentrant calls.

Explain why a loop is necessary without inventing a lost signal

Suppose two consumers wait for an item. A producer adds one item and broadcasts. Both consumers can leave the condition wait, but only one can first acquire the mutex and remove the item. When the other reacquires it, the predicate is false again. Returning directly from wait to an unconditional removal would be wrong even though the broadcast and mutex worked correctly.

A single signal also does not reserve the resource against another legitimate consumer that can run before the awakened thread acquires the mutex. The loop is therefore a resource-ownership requirement, not merely a defensive comment about theoretical spurious wakes.

In the tagged implementation, signal unpends a waiting thread if one exists; it does not store a future work token when the wait queue is empty. That is safe when the predicate retains the work: a consumer arriving later checks the predicate first and need not wait for an old signal.

Do not manually split unlock from wait

A tempting sequence is to check the predicate, unlock the mutex, and only then enter a separate wait. Between unlocking and waiting, a producer can update the state and signal while the consumer is not yet waiting. The combined condition-variable wait operation exists to close that boundary when used with the correct mutex protocol.

Do not “fix” the gap by adding a delay, querying thread state or assuming the producer’s lower priority prevents it from running. Keep the check under the mutex and use the supported atomic unlock-and-wait call. Verify wrappers have not inserted a manual unlock before it.

Check recursive depth and timeout return separately

Zephyr mutexes are reentrant, and the 3.7.0 mutex documentation requires matching unlocks before another thread can claim a repeatedly locked mutex. The condition-variable source performs one mutex unlock before waiting. Enter this protocol with one acquisition level; a hidden extra level can keep the mutex owned while the consumer waits and prevent the producer from making the predicate true.

The same implementation reacquires the mutex using K_FOREVER after the condition wait finishes. Consequently, a finite condition-wait timeout is not by itself a strict upper bound on the entire call’s return latency when another thread holds the mutex. Keep lock-hold time and reacquisition evidence in deadline analysis.

The API documents -EAGAIN for a timed-out wait. Define whether data that appears before mutex reacquisition may still satisfy the application request, or whether the expired deadline takes precedence. Either policy needs the protected predicate and an explicit decision; a return code alone should not authorize consuming an absent item.

Hypothetical example: broadcast wakes two workers for one buffer

Imagine a hypothetical processing service with two worker threads and one completed input buffer. Its producer broadcasts whenever it publishes. Both workers assume a successful wait return means they own a buffer, so one removes it while the other dereferences an empty-list entry.

The correction is a predicate loop with removal under the shared mutex. The second worker sees no item and waits again, or exits if stopping is true. Changing broadcast to signal may reduce wakeups but does not replace the ownership proof or the recheck.

Qualify the protocol with controlled competition

The following are proposed tests, not executed Zephyr target results: publish before any consumer waits; publish at the check/wait boundary; broadcast one item to two waiting consumers; let another consumer win mutex reacquisition; expire a wait while the producer holds the mutex; and request shutdown with no items.

Record predicate state, mutex ownership, wait result and the exact item claim. Require each published item to have at most one successful claim and every no-item wake to return to a valid wait or terminal decision. Also audit that no wait is entered with nested ownership. Deliver those traces with the pinned configuration and object lifetimes. Passing the ordinary one-producer/one-consumer path alone does not establish the competing-consumer contract.

Similar Posts