Spinlocks and hardware atomic instructions provide the fundamental building blocks of synchronization, but they suffer from a crippling operational drawback: busy waiting.
While a thread spins in a while loop waiting for a lock, it burns 100% of its CPU time slice doing zero productive work. If the thread holding the lock is blocked waiting for disk I/O, network packets, or timer events, spinning on the CPU achieves nothing.
To build scalable systems, operating systems introduce blocking synchronization primitives: mechanisms that transition waiting threads out of the CPU execution pipeline and into the BLOCKED queue until the required condition is fulfilled.
In Module 02, we explored how hardware atomic instructions enable spinlocks. While spinlocks provide rapid mutual exclusion for tiny, nanosecond critical sections on multiprocessor systems, they suffer from a fatal operational defect: busy waiting.
A spinning thread burns 100% of its CPU time slice executing instructions that achieve zero computational work. If the thread holding the lock is descheduled, preempted, or waiting on a slow peripheral (such as an NVMe disk read, network packet, or user keystroke), all competing threads on other cores spin fruitlessly, driving system temperature and power consumption to maximum while stalling progress.
To build scalable operating systems, software requires blocking primitives: mechanisms that allow a thread to release its CPU execution core voluntarily, enter a suspended state (BLOCKED or WAITING), and sleep until another execution thread notifies it that the desired event or resource has become available.
The earliest blocking synchronization models relied on a pair of paired operating system primitives:
sleep(): An atomic system call that causes the invoking process to suspend execution. The kernel removes the process from the scheduler's READY runqueue, updates its state to BLOCKED, and context-switches to another runnable thread.wakeup(pid): A system call taking a target process identifier (PID) as an argument. The kernel locates the target process's Process Control Block (PCB) and transitions its state from BLOCKED back to READY, placing it back onto the scheduler's runqueue.
To evaluate sleep() and wakeup(), consider the canonical Producer-Consumer problem (also called the Bounded-Buffer problem). Two processes share a common fixed-size buffer of capacity N:
count tracks the number of items currently residing in the buffer (initialized to 0)./* FLAWED: Producer-Consumer Implementation Using Primitive Sleep and Wakeup */
#define N 100 /* Number of slots in the shared buffer */
volatile int count = 0; /* Number of data items currently in buffer */
void producer(void) {
while (true) {
item_t item = produce_item(); /* Generate next data item */
if (count == N) {
sleep(); /* Buffer is full: producer sleeps */
}
insert_item(item); /* Deposit item into buffer */
count++; /* Increment item counter */
if (count == 1) {
wakeup(consumer_pid); /* Was empty: wake up sleeping consumer */
}
}
}
void consumer(void) {
while (true) {
if (count == 0) {
sleep(); /* Buffer is empty: consumer sleeps */
}
item_t item = remove_item(); /* Extract item from buffer */
count--; /* Decrement item counter */
if (count == N - 1) {
wakeup(producer_pid); /* Was full: wake up sleeping producer */
}
consume_item(item); /* Process the data item */
}
}
This intuitive code contains a fatal synchronization flaw known as the Lost Wakeup Problem. The defect arises because the high-level test of count and the execution of sleep() are not atomic.
count == 0). The Consumer executes if (count == 0). The condition evaluates to true. The Consumer prepares to execute sleep().
sleep(), the local APIC hardware timer generates an interrupt. The OS kernel context-switches from the Consumer to the Producer.
count from 0 to 1.
if (count == 1). Finding it true, the Producer calls wakeup(consumer_pid).
sleep(), the Consumer is not sleeping—it is in the READY state on the runqueue! A primitive wakeup signal has no memory buffer; because the process is not in the BLOCKED state, the wakeup signal is discarded by the kernel and permanently lost.
sleep() instruction. It executes sleep(), moving its PCB to the BLOCKED queue. The Consumer is now asleep, waiting for a wakeup signal that was already delivered and lost.
count. Because the Consumer is asleep, no items are ever removed. Eventually, the buffer fills completely (count == N). The Producer tests if (count == N), finds it true, and calls sleep().
Result: Total, unrecoverable deadlock. Both processes are permanently asleep, neither can run to wake the other, and no exception, crash log, or fault is raised by the operating system.
In the Windows NT kernel architecture, threads do not sleep via raw, uncoordinated signals. Instead, synchronization is mediated through Kernel Dispatcher Objects.
To solve the producer-consumer signaling problem cleanly, Windows introduces the Event Object (CreateEvent), which represents a persistent kernel state machine rather than an ephemeral pulse:
SynchronizationEvent): Initialized via CreateEvent(NULL, FALSE, FALSE, NULL). When signaled via SetEvent(), the kernel wakes exactly one thread waiting in WaitForSingleObject() and automatically resets the event state back to nonsignaled. If no thread is currently waiting, the event remains in the signaled state until a thread arrives and consumes it, completely averting the lost wakeup defect!NotificationEvent): Initialized via CreateEvent(NULL, TRUE, FALSE, NULL). When signaled via SetEvent(), the event remains signaled until explicitly cleared via ResetEvent(), waking all currently and subsequently waiting threads (broadcasting)./* Safe Win32 Producer-Consumer Signaling via Auto-Reset Event */
#include <windows.h>
HANDLE hEventItemReady;
void init_events(void) {
/* Auto-reset event (bManualReset = FALSE), initially non-signaled */
hEventItemReady = CreateEvent(NULL, FALSE, FALSE, NULL);
}
void consumer_worker(void) {
/* Atomic check & sleep: if event is signaled, consumes signal without sleeping */
DWORD wait_result = WaitForSingleObject(hEventItemReady, INFINITE);
if (wait_result == WAIT_OBJECT_0) {
consume_item(); /* Safe: event reset to 0 atomically */
}
}
void producer_worker(void) {
produce_item();
SetEvent(hEventItemReady); /* Signal remembered even if consumer not yet waiting */
}
SleepEx(dwMilliseconds, bAlertable).
SleepEx(INFINITE, TRUE), it places itself into an alertable wait state. The kernel will wake the thread not only when the timeout expires, but also when an Asynchronous Procedure Call (APC) is queued to the thread's kernel APC queue (e.g., when an asynchronous overlapped I/O packet completes). This allows a single worker thread to sleep efficiently while serving completion routines.
An intuitive early attempt to patch this flaw was to equip each process control block with a wakeup waiting bit:
wakeup() signal was sent to a process that was not yet asleep, the kernel set the target's wakeup waiting bit to 1.sleep(), the kernel checked this bit. If the bit was 1, the kernel cleared the bit to 0 and allowed the process to continue running without sleeping.1 on the first wakeup, but cannot represent the second or third wakeup (it saturates at 1).Consumer observes count=0 and determines it must sleep until items arrive.
Consumers must not read from empty buffers. Primitive sleep/wakeup assumes sleep will execute immediately.
In 1965, Dutch computer scientist Edsger W. Dijkstra introduced the Semaphore in his pioneering work on the THE multiprogramming system. The semaphore permanently resolved the lost wakeup problem by replacing stateless signaling pulses with an atomic integer variable coupled with a sleep queue.
A semaphore is not simply a flag; it is an abstract data type with historical memory. It maintains an integer counter that records wakeups sent in the past for consumption in the future.
In Dijkstra's original Dutch notation, the two fundamental operations were named P and V:
down() or wait(). Decrements the semaphore value and blocks the calling thread if resources are unavailable.up(), signal(), or post(). Increments the semaphore value and wakes a sleeping thread if any are queued.Operating system literature and production kernels define the integer counter behavior using two mathematical conventions:
In Dijkstra's original specification:
s is never negative (s ≥ 0).down() decrements s and proceeds.down() suspends the thread on the wait queue without decrementing.In modern operating system kernels:
s can become negative.down() always decrements s unconditionally. If s < 0, the thread is enqueued and blocked.up() increments s. If s ≤ 0, at least one thread is sleeping and must be awakened.
Students frequently encounter a conceptual puzzle: "If semaphores exist to eliminate busy waiting, how do we make the semaphore's own down() and up() routines atomic without busy waiting?"
The answer reveals a fundamental engineering pattern in kernel design: we use a spinlock to implement a sleep lock.
/* Production Kernel Semaphore Implementation */
typedef struct {
spinlock_t lock; /* Hardware spinlock protecting this struct */
int count; /* Resource permit counter */
struct list_head wait_list; /* FIFO queue of sleeping task_struct PCBs */
} semaphore_t;
void down(semaphore_t *sem) {
spin_lock(&sem->lock); /* 1. Acquire spinlock: protect counter and list */
sem->count--;
if (sem->count < 0) {
/* 2. Enqueue calling thread into semaphore wait list */
enqueue_current_task(&sem->wait_list);
set_current_task_state(TASK_UNINTERRUPTIBLE);
spin_unlock(&sem->lock);/* 3. RELEASE spinlock BEFORE invoking scheduler! */
schedule(); /* 4. Voluntary context switch: thread sleeps */
return;
}
spin_unlock(&sem->lock); /* Resource acquired immediately; release spinlock */
}
void up(semaphore_t *sem) {
spin_lock(&sem->lock); /* 1. Acquire spinlock */
sem->count++;
if (sem->count ≤ 0) {
/* 2. Dequeue first sleeping thread from FIFO list */
struct task_struct *task = dequeue_task(&sem->wait_list);
wake_up_task(task); /* 3. Transition PCB to TASK_RUNNING */
}
spin_unlock(&sem->lock); /* 4. Release spinlock */
}
sem->lock) is held only for the dozens of nanoseconds required to decrement an integer and insert a pointer into a linked list. It is never held across the schedule() system call.
Depending on initial configuration, semaphores fulfill three distinct architectural roles:
1.1 (unlocked) and 0 (locked). Ensures that only one thread can execute within a critical section at any instant.lock() is legally allowed to call unlock(). If a thread attempts to unlock a mutex it does not own, the kernel throws an exception. Furthermore, mutexes support Priority Inheritance to eliminate priority inversion.
up() to signal and release a binary semaphore.
down() allocates a permit; each up() returns a permit to the pool. When all permits are exhausted, subsequent requesters block automatically.0.down() and blocks immediately. Thread A carries out hardware initialization or loads data from disk, and then executes up(). This guarantees that Thread B cannot execute its post-requisite code until Thread A has completed its pre-requisite code.In the Windows operating system, both semaphores and mutexes are implemented as first-class Kernel Executive Objects accessible via Win32 API handles.
A Windows semaphore object maintains a 32-bit signed integer count restricted between 0 and a specified lMaximumCount:
/* Win32 Counting Semaphore Creation */
HANDLE hSem = CreateSemaphore(
NULL, /* Default security attributes */
3, /* Initial count (e.g., 3 vacant buffer slots) */
3, /* Maximum count capacity */
"Local\\BufferSem" /* Optional name for cross-process sharing */
);
/* P() / down() equivalent: */
WaitForSingleObject(hSem, INFINITE);
/* V() / up() equivalent: releases 1 permit and captures previous count */
LONG prev_count;
ReleaseSemaphore(hSem, 1, &prev_count);
A Windows Mutex (CreateMutex) is strictly a binary mutual exclusion primitive with ownership tracking. Only the thread that successfully acquired the mutex via a wait function can release it with ReleaseMutex().
Critically, Windows incorporates a vital kernel-level safety mechanism absent in raw POSIX semaphores: Automatic Abandoned Mutex Detection:
ETHREAD block. Upon thread termination, the kernel walks the thread's list of held mutexes, automatically releases them, and transitions the next waiting thread with a special status code:
DWORD wait_result = WaitForSingleObject(hMutex, INFINITE);
if (wait_result == WAIT_ABANDONED) {
/* The lock was acquired, BUT the previous owner died mid-critical section! */
/* Data structure may be partially corrupted; thread must audit consistency */
}
This architectural resilience allows enterprise server software to recover gracefully from catastrophic thread failures.
Counting semaphores are initialized to buffer capacity (empty=3) and available items (full=0).
Permit counting decouples producer and consumer burst rates up to the queue capacity.
In Section 2, we analyzed how operating system kernels implement semaphores and sleep locks using internal spinlocks to guard wait queues. However, that design introduces a major performance bottleneck for user-space applications: every lock acquisition and release requires a kernel system call.
A system call trap instruction (syscall on x86-64 Linux, syscall / sysenter on Windows, svc on ARM) forces the processor through an involuntary privilege boundary transition from Ring 3 (User Space) to Ring 0 (Kernel Mode). The CPU flushes pipeline queues, changes the stack pointer to the kernel stack, updates page-table permissions, and validates parameters. This sequence consumes 100 to 300 nanoseconds per invocation.
Extensive benchmarking of production software (databases, web servers, GUI toolkits) reveals a universal concurrency profile:
Paying a 200 ns system call penalty millions of times per second just to set an uncontended integer flag in memory is an immense waste of CPU cycles. Conversely, relying purely on spinlocks in user space wastes entire scheduling quanta when a lock is contended.
To bridge this performance divide, modern operating systems implement hybrid user/kernel synchronization:
futex (Fast Userspace Mutex) architecture.CRITICAL_SECTION, SRWLOCK, and WaitOnAddress.
A Linux futex is an ordinary 32-bit integer allocated in user-space virtual memory. The fast-path executes an atomic Compare-and-Swap (cmpxchg) in Ring 3 (~5 ns). Only if contention occurs does the thread drop into Ring 0 via:
syscall(SYS_futex, uaddr, FUTEX_WAIT, val, timeout, NULL, 0);
The kernel verifies that *uaddr == val atomically under a bucket spinlock before putting the task to sleep on a kernel hash table (futex_queues), eliminating lost wakeups.
Windows pioneered user-mode hybrid locking and has developed three distinct generations of high-performance synchronization:
The classic Win32 user-mode lock. It wraps an internal RTL_CRITICAL_SECTION struct.
Adaptive Spin Optimization:
Configured with InitializeCriticalSectionAndSpinCount(&cs, 4000). On multi-core CPUs, it spins in a tight user-mode loop up to 4,000 times before allocating a kernel auto-reset event object to sleep.
Reentrancy: Supports recursive acquisition by the same thread.
Introduced in Windows Vista to replace heavy critical sections.
Pointer-Sized Efficiency: Consumes only 4 bytes (32-bit) or 8 bytes (64-bit). Requires no dynamic initialization or destructor (SRWLOCK_INIT).
Dual-Mode: Supports both shared reader locks (AcquireSRWLockShared) and exclusive writer locks (AcquireSRWLockExclusive).
Non-Reentrant: Recursion is forbidden, eliminating bookkeeping overhead.
Introduced in Windows 8 / Server 2012 (synchapi.h).
Turns any 1, 2, 4, or 8-byte variable in user space into a sleep-wait target.
API:
WaitOnAddress(&addr, &undesired, size, timeout)
WakeByAddressSingle(&addr)
WakeByAddressAll(&addr)
Sleeps in the kernel only if *addr == undesired, matching Linux futex semantics.
| Metric / Capability | Linux Futex Subsystem | Windows NT User-Mode Subsystem |
|---|---|---|
| Lightweight User Mutex | pthread_mutex_t (built on futex) | SRWLOCK (Slim Reader/Writer Lock) |
| Direct Memory Wait Primitive | syscall(SYS_futex, uaddr, FUTEX_WAIT, val) | WaitOnAddress(addr, &undesired, size, timeout) |
| Direct Wake Primitive | syscall(SYS_futex, uaddr, FUTEX_WAKE, 1) | WakeByAddressSingle(addr) |
| Supported Target Data Sizes | Historically 32-bit only (FUTEX2 adds 8/16/64) | 1, 2, 4, or 8 bytes natively |
| Wait Queue Implementation | Global kernel hash table (futex_queues) | Kernel keyed-event synchronization buckets |
| C++20 Standard Mapping | std::atomic::wait() → FUTEX_WAIT | std::atomic::wait() → WaitOnAddress |
We can construct an industrial-grade, pointer-sized mutex on Windows without allocating Win32 handles or kernel objects using WaitOnAddress:
/* Production Windows Fast Mutex Using WaitOnAddress (synchapi.h) */
#include <windows.h>
#include <synchapi.h>
typedef struct {
volatile LONG state; /* 0 = Free, 1 = Held (no waiters), 2 = Held (waiters present) */
} win_fast_mutex_t;
void win_fast_mutex_lock(win_fast_mutex_t *m) {
/* 1. FAST PATH: Optimistically attempt atomic CAS 0 -> 1 in user space */
LONG prev = InterlockedCompareExchange(&m->state, 1, 0);
if (prev == 0) {
return; /* Lock acquired in Ring 3! ~5 nanoseconds, zero syscalls */
}
/* 2. SLOW PATH: Contention detected */
while (true) {
/* Announce that waiters exist (state = 2) */
if (prev == 2 || InterlockedCompareExchange(&m->state, 2, 1) != 0) {
/* Sleep in kernel if state is still 2 */
LONG undesired = 2;
WaitOnAddress(&m->state, &undesired, sizeof(LONG), INFINITE);
}
/* Try to acquire upon wakeup */
prev = InterlockedExchange(&m->state, 2);
if (prev == 0) break;
}
}
void win_fast_mutex_unlock(win_fast_mutex_t *m) {
/* Fast path: If state was 1, no waiters exist. Reset to 0 in user space */
if (InterlockedExchange(&m->state, 0) == 2) {
/* Waiters were present; wake up exactly one waiting thread */
WakeByAddressSingle((PVOID)&m->state);
}
}
Thread A executes atomic cmpxchg(uaddr, 0, 1) entirely inside Ring 3 user memory.
Uncontended locks should execute in single-digit nanoseconds without paying the overhead of kernel traps.
While semaphores and futexes provide powerful low-level primitives for operating system kernels, building large-scale, correct multi-threaded applications using unstructured semaphores is notoriously error-prone.
Consider the fragility of semaphore code in production systems:
down(&mutex) but omits up(&mutex) due to an early return or uncaught exception leaves the critical region permanently locked, deadlocking the application.To resolve these architectural weaknesses, C. A. R. Hoare (1974) and Per Brinch Hansen (1975) pioneered the Monitor: a high-level programming language construct that encapsulates shared variables, access procedures, and synchronization gates into a unified, compiler-enforced boundary.
A monitor is an object-oriented or module-level abstraction consisting of:
Compiler-enforced mutual exclusion creates a critical synchronization dilemma:
The In-Monitor Sleep Dilemma: If Thread 1 enters a monitor method and discovers that a necessary condition is not met (e.g., the bounded buffer is full), it cannot simply execute a standard sleep() system call. If Thread 1 went to sleep while holding the monitor's mutual exclusion lock, no other thread could ever enter the monitor. A consumer could never enter to extract an item, and the producer would sleep forever, freezing the application permanently.
To resolve this, monitors introduce Condition Variables (e.g., cond_t in C/POSIX, Condition in Java/C#):
wait(&cond, &mutex): Atomically releases the monitor lock and suspends the calling thread on the condition variable's wait queue. Because the monitor lock is released, other threads can now enter the monitor to mutate state. When the sleeping thread is eventually signaled and awakened, it automatically re-acquires the monitor lock before wait() returns to user code.signal(&cond): Wakes up exactly one thread waiting on the condition variable. If no threads are waiting, the signal is quietly discarded with zero side effects.broadcast(&cond): Wakes up all threads currently waiting on the condition variable.up() is called when no threads are waiting, the counter increments to 1. A subsequent down() consumes that saved permit without sleeping.signal(&cond) when no threads are currently suspended in wait(), the signal vanishes completely into the ether. It does not save credit for a future wait().Below is the classical bounded-buffer implemented via an explicit monitor pattern in C using POSIX threads:
/* Thread-Safe Bounded-Buffer Monitor using POSIX Threads */
#include <pthread.h>
#include <stdbool.h>
#define BUFFER_SIZE 100
typedef struct {
int buffer[BUFFER_SIZE];
int count; /* Number of populated slots */
int head; /* Write index pointer */
int tail; /* Read index pointer */
pthread_mutex_t lock; /* Monitor entry gatekeeper */
pthread_cond_t not_full; /* Condition: buffer has room for insert */
pthread_cond_t not_empty; /* Condition: buffer has item for removal */
} bounded_buffer_monitor_t;
void monitor_insert(bounded_buffer_monitor_t *m, int item) {
pthread_mutex_lock(&m->lock); /* 1. Enter Monitor (Acquire Mutex) */
/* 2. Wait while condition is not met (MESA INVARIANT: while loop!) */
while (m->count == BUFFER_SIZE) {
pthread_cond_wait(&m->not_full, &m->lock); /* Releases lock & sleeps */
}
/* 3. Mutate private state (Guaranteed Exclusive Access) */
m->buffer[m->head] = item;
m->head = (m->head + 1) % BUFFER_SIZE;
m->count++;
/* 4. Signal waiting consumers that buffer is no longer empty */
pthread_cond_signal(&m->not_empty);
pthread_mutex_unlock(&m->lock); /* 5. Exit Monitor (Release Mutex) */
}
int monitor_remove(bounded_buffer_monitor_t *m) {
pthread_mutex_lock(&m->lock); /* 1. Enter Monitor */
while (m->count == 0) {
pthread_cond_wait(&m->not_empty, &m->lock);
}
int item = m->buffer[m->tail];
m->tail = (m->tail + 1) % BUFFER_SIZE;
m->count--;
pthread_cond_signal(&m->not_full); /* Signal waiting producers */
pthread_mutex_unlock(&m->lock); /* Exit Monitor */
return item;
}
When a thread inside a monitor calls signal(&cond) and wakes a suspended thread, a fundamental concurrency question arises: Which thread is allowed to execute next? Both the signaling thread and the awakened thread now demand access to the monitor, but the monitor invariant permits only one thread inside!
This problem divided computer scientists into three distinct schools of monitor semantics:
The signaling thread immediately yields the monitor lock and CPU to the awakened thread.
The awakened thread runs instantaneously. Because no intervening thread could run, the predicate condition is guaranteed to be strictly true.
Code Pattern: Simple if checks are theoretically valid:
if (count == N)
wait(&cond);
Developed at Xerox PARC (Lampson & Redell, 1980). The signaling thread retains the lock and continues running until it leaves the monitor.
The awakened thread is moved to the monitor's entry queue. When it finally reacquires the lock, another thread may have sneaked in and invalidated the condition!
The Invariant: Threads MUST re-check conditions in a while loop:
while (count == N)
wait(&cond);
A compromise design: a thread is permitted to call signal() only as the very final instruction before returning from the monitor procedure.
Because the signaler exits immediately, lock ownership transfers cleanly to the awakened thread without creating two active contenders.
Drawback: Restrictive programming model; prevents sending signals mid-calculation.
In production systems programming, replacing while (!condition) with if (!condition) around a condition wait is considered a critical bug. There are three distinct engineering reasons why threads must always re-check conditions in a loop:
pthread_cond_broadcast(), multiple sleeping threads wake up. The first thread to acquire the lock claims the available slot; all subsequent threads awaken to find the condition false once again.
SIGINT or SIGALRM) interrupting a sleep primitive, multi-core cache invalidation races in the futex hash table, or context switch preemption during wait queue rebalancing.
/* NEVER WRITE THIS (BUG): */
if (!condition_is_met) {
pthread_cond_wait(&cond, &mutex);
}
/* ALWAYS WRITE THIS (CORRECT & SAFE): */
while (!condition_is_met) {
pthread_cond_wait(&cond, &mutex);
}
In Windows Vista, Microsoft introduced native support for condition variables to match POSIX capabilities without requiring handle allocations:
CONDITION_VARIABLE cv;). Initialized statically via CONDITION_VARIABLE_INIT or dynamically via InitializeConditionVariable(&cv).pthread_mutex_t, Windows condition variables can atomically unlock and sleep over either a Critical Section or an SRW Lock:
SleepConditionVariableCS(&cv, &cs, dwMilliseconds): Releases Critical Section, sleeps, and re-acquires upon wake.SleepConditionVariableSRW(&cv, &srw, dwMilliseconds, flags): Releases SRW lock (shared or exclusive), sleeps, and re-acquires.WakeConditionVariable(&cv): Equivalent to pthread_cond_signal (wakes one waiter).WakeAllConditionVariable(&cv): Equivalent to pthread_cond_broadcast (wakes all waiters)./* Windows Bounded-Buffer Monitor with SRWLock and CONDITION_VARIABLE */
#include <windows.h>
#include <stdbool.h>
#define BUFFER_CAPACITY 50
typedef struct {
int buffer[BUFFER_CAPACITY];
int count, head, tail;
SRWLOCK lock; /* Fast user-mode lock */
CONDITION_VARIABLE cv_not_full; /* Condition: room for producer */
CONDITION_VARIABLE cv_not_empty; /* Condition: items for consumer */
} win_monitor_t;
void win_monitor_insert(win_monitor_t *m, int item) {
AcquireSRWLockExclusive(&m->lock);
/* MESA SEMANTICS INVARIANT: Mandatory while loop re-check */
while (m->count == BUFFER_CAPACITY) {
SleepConditionVariableSRW(&m->cv_not_full, &m->lock, INFINITE, 0);
}
m->buffer[m->head] = item;
m->head = (m->head + 1) % BUFFER_CAPACITY;
m->count++;
WakeConditionVariable(&m->cv_not_empty); /* Wake one consumer */
ReleaseSRWLockExclusive(&m->lock);
}
int win_monitor_remove(win_monitor_t *m) {
AcquireSRWLockExclusive(&m->lock);
while (m->count == 0) {
SleepConditionVariableSRW(&m->cv_not_empty, &m->lock, INFINITE, 0);
}
int item = m->buffer[m->tail];
m->tail = (m->tail + 1) % BUFFER_CAPACITY;
m->count--;
WakeConditionVariable(&m->cv_not_full); /* Wake one producer */
ReleaseSRWLockExclusive(&m->lock);
return item;
}
Windows condition variables strictly adhere to Mesa semantics (Signal-and-Continue). The thread calling WakeConditionVariable retains the SRW lock and continues executing; the awakened thread competes with incoming threads when re-acquiring the lock. Consequently, the while loop predicate guard is strictly mandatory on Windows.
Producer T2 finishes insertion and signals the condition variable.
Under Mesa semantics, signaling transitions the waiter to READY without immediately yielding the CPU.