Before you start#
Review locks and sleeping, futexes, and wineserver handles. This chapter pins upstream Wine wine-10.0 to commit b073859675060c9211fcbccfd90e4e87520dc2c2. Linux v6.12 is a separate historical kernel baseline. All wait sequences below are synthetic; no Wine executable or guest was run.
Learning goals#
Compare event, mutex and semaphore state; trace Wine's wait selection and satisfaction; distinguish timeout and abandoned ownership; and explain why a Linux futex is a lower-level building block rather than a Windows wait object.
Source walkthrough#
- Waiting threadWine wait path
Submit handles, wait mode and timeout
- Wine wait pathServer objects
Check readiness and retain wait state
- Server objectsWine wait path
Return. Signal, abandonment or timeout
- Wine wait pathWaiting thread
Return. Return a result to interpret
Time runs downward. Dashed arrows show returns.
Read the text version
- Waiting thread → Wine wait path: Submit handles, wait mode and timeout
- Wine wait path → Server objects: Check readiness and retain wait state
- Server objects → Wine wait path (return): Signal, abandonment or timeout
- Wine wait path → Waiting thread (return): Return a result to interpret
Windows-facing synchronization objects represent different state machines. Microsoft documents manual-reset and auto-reset event behavior in Event Objects, and semaphore/mutex wait side effects in Wait Functions. A manual-reset event remains signaled until reset; while signaled it can satisfy multiple eligible waits. An auto-reset event represents a single signaled state that is consumed by one successful waiter. It is not a counter. A semaphore stores a count bounded by a maximum; a successful wait consumes one count and a release increases the count subject to that maximum. A mutex associates ownership with a thread, supports recursive acquisition by its owner, and must be released with the corresponding ownership rules; each successful recursive acquisition requires a matching release. Mutex Objects. These differences matter even if all three can wake a waiting thread.
- Manual-reset event
- Stays signaled after a satisfied wait until something resets it; can satisfy several waits.
- Auto-reset event
- A satisfied wait clears the flag; a second set does not add a count.
- Semaphore
- Each wait consumes one count; a release that exceeds the maximum fails.
- Mutex
- Recursive for its owner; an abandoned owner makes the next acquisition report abandonment.
Wine's server-side objects implement operations such as signaled and satisfied. A wait first determines whether the selected objects can satisfy the request; satisfaction then applies object-specific state changes. A handle is resolved through the process's handle table with required access and an object reference, as discussed in wineserver handles. An invalid handle or missing synchronization right can fail before a wait is placed on an object. Closing a handle is not a substitute for coordinating an outstanding wait; object references and wait queues have their own lifetimes.
In server/event.c, event_satisfied contains this exact Wine 10.0 excerpt. The file's notice grants LGPL-2.1-or-later; surrounding type assertion and function boundary are omitted:
/* Reset if it's an auto-reset event */
if (!event->manual_reset) event->signaled = 0;
The condition distinguishes auto-reset from manual-reset state. On satisfaction of an auto-reset event, the server clears its signaled flag; a manual-reset event remains signaled until another operation resets it. The adjacent event_signaled returns the flag. The set_event function sets it to one and asks the server to wake eligible waiters; setting an already signaled event does not increment a hidden count. A wake notification is a reason to re-evaluate a wait under the server's protocol, not a license for a client to assume that any arbitrary object is ready.
Wait-any, wait-all and server state#
In dlls/ntdll/unix/sync.c, NtWaitForMultipleObjects validates the count, constructs a select operation, marks it alertable if requested, translates handles to server handle representation and calls server_wait. This exact Wine 10.0 LGPL-2.1-or-later excerpt omits the surrounding validation and handle-copy loop:
if (alertable) flags |= SELECT_ALERTABLE;
select_op.wait.op = wait_any ? SELECT_WAIT : SELECT_WAIT_ALL;
The first statement adds the alertable flag only when requested. The second chooses wait-any or wait-all selection. The function does not itself loop through Linux futex words in this branch; the server-wait path has its own request and wake protocol. A count of zero or greater than the implementation's maximum returns STATUS_INVALID_PARAMETER_1 before this setup. The source's selected backend and Wine build are relevant; do not attribute downstream Proton esync/fsync patches or a later kernel NTSYNC driver to every upstream Wine 10.0 installation or to Linux v6.12.
The server's thread wait implementation uses a thread_wait and queue entries holding the objects involved. check_wait checks all entries for a wait-all request, or finds a signaled entry for wait-any. In the wait-all case it tests all objects and returns the satisfied status only when none is unsignaled; end_wait then invokes each object's satisfied operation. If the full condition is not met, it does not consume an auto-reset event merely because that one event was signaled during an unsuccessful check. Object-specific signaled callbacks can have their own behavior, so “atomic” here means the server's coordinated wait decision and satisfaction under its implementation, not an assertion that arbitrary callbacks have no side effects.
For wait-any, a successful status identifies the selected index, conventionally represented by STATUS_WAIT_0 + n at the NT boundary. Wait-all returns the relevant success status for the complete set. A timeout returns STATUS_TIMEOUT; an alertable wait can return STATUS_USER_APC when a queued user APC is delivered under the appropriate conditions. An abandoned mutex is reported using the abandoned-wait status range, indicating that the previous owner terminated without releasing it and that the new owner must inspect protected state. Actual user-facing Win32 wrappers have their own return names and conversions.
A timeout does not mean the object was destroyed, and an APC result does not prove the requested object became signaled. In the server code, completion removes wait-queue entries and timeout state before freeing the wait structure. Object references and pending wake messages must remain valid until the wait is completed or cancelled. A client thread exiting, a server connection failure or a timeout can race with a signal. The correct outcome comes from the implementation's ordering and cleanup, not from a guess based on log order.
Worked event and semaphore sequence#
Start with an auto-reset event E in the nonsignaled state and no waiters. Call SetEvent twice before any wait. In the source model each call sets the Boolean signaled state to one; the second call does not create a second token. The first successful wait observes the signaled state and event_satisfied clears it. A second zero-timeout wait finds it nonsignaled and can return timeout unless another signal intervenes. This is a synthetic state sequence derived from the cited implementation; no Wine call ran here.
Contrast a semaphore S with initial count zero and maximum at least two. Two successful releases of one raise the count to two. Two successful waits each consume one count, leaving zero; a third zero-timeout wait cannot acquire a count. Releasing beyond the maximum fails with the relevant semaphore-limit status rather than wrapping or silently adding unlimited tokens. The distinction explains why an auto-reset event is unsuitable when the application must count every independent unit of work.
The limit is enforced in the server. In server/semaphore.c, release_semaphore in Wine 10.0 (LGPL-2.1-or-later) rejects an invalid count before it changes state. The wake-up branch that follows is omitted:
if (sem->count + count < sem->count || sem->count + count > sem->max)
{
set_error( STATUS_SEMAPHORE_LIMIT_EXCEEDED );
return 0;
}
A rejected release leaves sem->count unchanged and sets the limit status. The first test also catches unsigned overflow of the sum, so a very large count cannot wrap into a small legal value. The excerpt does not show how the caller reports the failure to the application, so a release that returns an error must be treated as not having added any tokens.
Now consider wait-all on E and S. E is signaled, S has count zero, and there is no concurrent operation. The server checks the set and cannot satisfy the whole wait. E is not consumed simply to discover that S is unavailable. If the wait times out, E can remain signaled under those assumptions. If S is later released and both are available, the successful wait-all satisfaction clears the auto-reset event and consumes one semaphore count. If another thread concurrently changes either object, the actual server ordering determines the outcome; the simple timeline cannot predict it without that ordering.
- Waiting threadServer wait
Wait for E and S together, wait-all
- Server waitEvent E
Query signaled state
- Event EServer wait
Return. E is signaled; not consumed yet
- Server waitSemaphore S
Query count
- Semaphore SServer wait
Return. Count is zero; not satisfiable
- Server waitWaiting thread
Return. Time out; E stays signaled
Time runs downward. Dashed arrows show returns.
Read the text version
- Waiting thread → Server wait: Wait for E and S together, wait-all
- Server wait → Event E: Query signaled state
- Event E → Server wait (return): E is signaled; not consumed yet
- Server wait → Semaphore S: Query count
- Semaphore S → Server wait (return): Count is zero; not satisfiable
- Server wait → Waiting thread (return): Time out; E stays signaled
Mutex ownership, recursion and abandonment#
In server/mutex.c, mutex_signaled treats the mutex as available when its count is zero or when the waiting thread is already its owner. mutex_satisfied calls the acquisition helper, marks the wait abandoned if the object carries its abandoned state, then clears that state. Recursion means an owning thread can acquire again and must release enough times to relinquish ownership. A different thread cannot legitimately release an owned mutex; the source has an error path for a non-owner.
The satisfaction function is short. In server/mutex.c, Wine 10.0, LGPL-2.1-or-later:
static void mutex_satisfied( struct object *obj, struct wait_queue_entry *entry )
{
struct mutex *mutex = (struct mutex *)obj;
assert( obj->ops == &mutex_ops );
do_grab( mutex, get_wait_queue_thread( entry ));
if (mutex->abandoned) make_wait_abandoned( entry );
mutex->abandoned = 0;
}
The abandoned flag is consumed by the satisfaction that observes it, so only the first waiter after an abandonment receives the abandoned result; later waiters see an ordinary acquisition. The function does not inspect the protected data. That check remains the application's recovery obligation, described below.
Imagine thread A owns a mutex and terminates without releasing it. Under the server's abandonment handling the object can become available with an abandoned indication. Thread B's next successful wait acquires the mutex but receives an abandoned status. B must treat the protected data as potentially inconsistent and apply the application's recovery policy; the status is not an ordinary timeout and does not mean the mutex remained owned by A. A wait-all involving an abandoned mutex has additional result interpretation; inspect the returned status and object ordering rather than assuming every object is healthy.
A mutex's server reference and the lifetime of data it protects are separate. Closing the last visible handle while another server operation holds a reference or wait entry does not imply immediate destruction. A process that closes a handle being used concurrently must follow the Windows/Wine API contract; numeric handle reuse can make a stale value refer to a different object. Server object lifetime and application data recovery are both required to reason about the race.
Linux futexes are lower-level#
A Linux futex is a 32-bit user-memory word used with kernel wait/wake operations. FUTEX_WAIT atomically compares the word with the expected value and begins sleeping only if it still matches; otherwise it returns EAGAIN. A successful wake does not reserve the application's condition until the awakened thread runs: another thread may change the predicate first. The caller must therefore use atomic state updates with appropriate memory ordering and loop to recheck its predicate after every return. Signals and timeouts are additional wait outcomes. Linux futex(2), FUTEX_WAIT(2const). A futex wake count is not automatically a Windows semaphore count or an event's persistent signaled state.
Wine may use host synchronization primitives in different components, but the cited NtWaitForMultipleObjects path constructs a server selection. Do not replace that verified path with an assumed universal futex call chain. Likewise, Proton-specific esync/fsync approaches are downstream choices and are not automatically upstream Wine 10.0 behavior. Linux v6.12 cannot be assumed to provide a later NTSYNC driver. An actual build, configuration and source path are necessary before claiming a particular backend was used.
Timeouts introduce another boundary. Relative and absolute timeout representations and clock domains differ across interfaces, and Win32 timeout behavior has changed across OS generations (including whether time in low-power states counts). A waiting thread can also be delayed by host scheduling after a wake, so elapsed wall time need not equal the requested timeout exactly. WaitForMultipleObjectsEx. Alertable waits can process APCs and return a distinct status; an application must decide whether to retry while preserving its remaining deadline. Retrying with the full original timeout after every APC can extend the total wait beyond the intended bound.
Designing a useful wait investigation#
Imagine that thread A owns a mutex and is about to update a shared queue, while thread B waits for both that mutex and an auto-reset event. A trace that says only “B woke” is insufficient: first identify which handles B submitted, whether the wait is any or all, whether the event was already signaled, and whether A released or abandoned the mutex. A wait-all succeeds only when the combined condition can be satisfied; checking the event first is not permission to consume its one signal while the mutex remains unavailable. If the timeout occurs first, B must treat the operation as timed out and must not pretend that it acquired the mutex. This is why a useful state table records ownership and signal state both before and after each candidate transition.
For an abandoned mutex, successful acquisition tells the new owner that the previous owner died while holding it. It does not certify that the queue update finished. B needs a queue-specific recovery rule: validate or reconstruct invariants under the newly acquired mutex, or reject the data. Merely releasing the mutex immediately and continuing could make corrupted state visible to another waiter. If the same owner recursively acquires a mutex, a single release is insufficient to make it available to others; account for the recursion count separately from object references and handle count.
For an alertable wait, an APC completion can interrupt the expected waiting sequence. The caller must distinguish that outcome from a signaled object and decide whether to wait again with a revised deadline. Reusing the original relative timeout after every interruption can extend the total waiting period. Closing a handle listed in a pending WaitForMultipleObjectsEx call has undefined behavior under the Win32 contract; a numeric value later reused for an unrelated object is not evidence that the original wait acquired that replacement. WaitForMultipleObjectsEx. A reliable test coordinates handle closure after all waiters have completed and verifies each result separately.
Practice#
Synthetic state accounting
This own-process model compares an auto-reset event with a two-count semaphore. It is teaching arithmetic, not Wine or Linux synchronization code:
python3 - <<'PY'
event = False
for _ in range(2): event = True
for i in range(2):
acquired = event
if acquired: event = False
print('event wait', i + 1, 'acquired:', acquired)
count = 0
for _ in range(2): count += 1
for i in range(3):
acquired = count > 0
if acquired: count -= 1
print('semaphore wait', i + 1, 'acquired:', acquired, 'remaining:', count)
PY
Expected event results are true then false; semaphore results are true, true, false. The program has no threads, server, timeout or APC and cannot validate a real wait implementation. As a counterexample, treating the event's second set as a second count would incorrectly predict that both event waits succeed.
Source-only alternative: use the supplied E/S wait-all state. Write its state before an unsuccessful check, after timeout, and after S is released and a successful wait-all occurs. Add the abandoned-mutex case and identify the returned status category and data-recovery obligation. If Python is unavailable, calculate the same table by hand; do not initialize a default Wine prefix or install tools.
Knowledge check#
- Why do two sets of an already signaled auto-reset event not create two tokens?
- Why does the synthetic wait-all timeout not consume E when S remains unavailable?
- What does an abandoned mutex result tell the new owner?
- Why must a futex waiter recheck its predicate after waking or receiving
EAGAIN? - Does the cited Wine 10.0 wait path establish that a later NTSYNC kernel driver is used?
Answers#
- The source sets a Boolean signaled flag. A successful auto-reset satisfaction clears it; the second set did not increment a semaphore-like count.
- The server checks all objects and only invokes their satisfaction operations after the full wait-all condition succeeds, under the stated no-concurrency assumptions.
- The previous owner terminated without normal release; the new owner acquires the mutex with an abandoned indication and must assess the protected data's consistency.
- A wake or value mismatch does not reserve the application's condition for that waiter. Another thread can change the state, and signals or spurious wakeups are possible.
- No. The inspected function constructs a server wait. Backend behavior must be established for the actual upstream or downstream build and kernel.
Checkpoint#
Submit the event/semaphore table, wait-all timeline and abandoned-mutex recovery note. Ready means signaling is not confused with counting, failed wait-all does not consume the example event, and futex predicate and Wine server semantics remain distinct. Retry the source branches if a timeout is treated as object destruction. This is not certification.