--- a/gate.c +++ b/gate.c @@ -1,18 +1,28 @@ /* - * EXERCISE 03 — BROKEN STARTING POINT. Do not edit this file; copy it. + * EXERCISE 03 — FIXED VARIANT. The repair is a primitive that has an OWNER. * - * ============================ UNSAFE CODE WARNING ============================ - * This program uses a dispatch semaphore as a mutual-exclusion primitive. That - * is the defect you are being asked to find. Never ship it. - * ============================================================================= - * * THE SCENARIO - * A thumbnail cache is protected by a "lock" that somebody built out of a - * dispatch semaphore with an initial value of 1. It excludes correctly: only - * one thread is ever inside the critical section. A performance engineer - * nonetheless reports that user-interactive work waiting on this cache runs - * at the speed of whatever background thread happens to hold it. + * A thumbnail cache protected by a gate. Exactly one thread may be inside the + * critical section at a time. * + * THE REPAIR + * The dispatch semaphore is replaced by os_unfair_lock. Both exclude equally + * well; only one of them records WHICH THREAD is inside. + * + * os_unfair_lock stores the owning thread's port in the lock word. When a + * higher-priority thread blocks on it, the kernel knows precisely which + * thread to boost, and boosts it until the lock is released. A semaphore is a + * bare count with no owner — `signal` may legitimately come from a thread + * that never called `wait` — so there is nothing to boost. + * + * The rule this measurement supports: a semaphore is for COUNTING PERMITS or + * for signalling between threads. Mutual exclusion wants a lock, because only + * a lock can tell the kernel who is holding things up. + * + * pthread_mutex and Swift's Mutex / OSAllocatedUnfairLock carry ownership in + * the same way and behave the same way here. NSLock and NSRecursiveLock are + * built on pthread_mutex; DispatchSemaphore and DispatchGroup are not. + * * WHAT THIS PROGRAM MEASURES * A BACKGROUND-QoS thread takes the gate and holds it for 400 ms. Halfway * through, a USER_INTERACTIVE thread tries to take the same gate and blocks. @@ -29,17 +39,17 @@ * that owner while a higher-priority thread waits. The number goes up. If it * does not, the number does not move. * - * Build: clang -O0 -g -Wall -Wextra gate.c -o gate_broken - * Run: ./gate_broken [trials] (default 3) + * Build: clang -O0 -g -Wall -Wextra gate.c -o gate_fixed + * Run: ./gate_fixed [trials] (default 3) * - * Expected: donated=NO on every trial, and a summary line reading - * donatedTrials=0/3. Exit status 0 — nothing here crashes or hangs. This is a - * SCHEDULING defect, not a liveness one, which is exactly why it survives - * testing. + * Expected: donated=YES on the trials, with the holder's priority rising from + * the BACKGROUND band into the USER_INTERACTIVE band while the high-priority + * thread waits. Exit status 0. * - * NOT CLAIMED: this program does not produce the textbook unbounded - * priority-inversion stall. On modern Darwin the background holder still runs - * and still finishes. What is measured here is the donation signal itself. + * NOT CLAIMED: donation is not a guarantee you can assert to the instruction. + * It is a kernel policy observed here on this OS build, and a trial can miss it + * if the scheduler settles differently. The bundled check therefore requires a + * majority of trials to donate, not all of them. */ #include #include @@ -78,25 +88,29 @@ } /* --------------------------------- the gate -------------------------------- - * A counting semaphore initialised to 1, used for mutual exclusion. + * An os_unfair_lock, used for mutual exclusion. * - * It excludes correctly. What it does not do is record an OWNER: a semaphore is - * a bare count, and `signal` may legitimately come from a thread that never - * called `wait`. There is therefore no thread for the kernel to boost. + * It excludes exactly as the semaphore did. The difference is that it records + * an OWNER, so the kernel has a specific thread it can boost while a + * higher-priority thread is blocked on the lock. + * + * "Unfair" refers to hand-off policy, not to correctness: a waiter is not + * guaranteed to acquire in arrival order. That is a starvation consideration, + * separate from the donation behaviour measured here. * -------------------------------------------------------------------------- */ -typedef struct { dispatch_semaphore_t sem; } gate_t; +typedef struct { os_unfair_lock lock; } gate_t; -static const char *GATE_NAME = "dispatch_semaphore(1)"; +static const char *GATE_NAME = "os_unfair_lock"; static void gate_init(gate_t *g) { - g->sem = dispatch_semaphore_create(1); + g->lock = (os_unfair_lock)OS_UNFAIR_LOCK_INIT; } static void gate_acquire(gate_t *g) { - dispatch_semaphore_wait(g->sem, DISPATCH_TIME_FOREVER); + os_unfair_lock_lock(&g->lock); } static void gate_release(gate_t *g) { - dispatch_semaphore_signal(g->sem); + os_unfair_lock_unlock(&g->lock); } /* --------------------------------- a trial --------------------------------- */ @@ -151,14 +165,14 @@ pthread_detach(w); setvbuf(stdout, NULL, _IOLBF, 0); - printf("EX03 build=broken primitive=%s\n", GATE_NAME); + printf("EX03 build=fixed primitive=%s\n", GATE_NAME); printf(" holder QoS=BACKGROUND, waiter QoS=USER_INTERACTIVE\n"); printf(" signal = thread_info(THREAD_EXTENDED_INFO).pth_curpri\n"); int donated = 0; for (int i = 1; i <= trials; i++) donated += trial(i); - printf("EX03 build=broken primitive=%s trials=%d donatedTrials=%d\n", + printf("EX03 build=fixed primitive=%s trials=%d donatedTrials=%d\n", GATE_NAME, trials, donated); return 0; }