Exercise 3: One Unprotected Accessor Reopens the Race for Everyone — Possible Solution ==================================================================== THE TEST ------------------------------ # 'disciplined' does everything correctly: one_increment_protected = ['ACQUIRE', 'LOAD', 'INC', 'STORE', 'RELEASE'] # 'careless' skips the lock entirely: one_increment_UNPROTECTED = ['LOAD', 'INC', 'STORE'] programs[disciplined.pid] = one_increment_protected * 20 programs[careless.pid] = one_increment_UNPROTECTED * 20 # both run through the same shared counter, QUANTUM=1 RESULT ------------------------------ one process uses ACQUIRE/RELEASE correctly, the other skips it entirely: expected 40 (20 increments each), got 28 Real updates are lost -- 12 of the expected 40 increments vanish -- even though 'disciplined' followed every rule Chapter 2 established: acquire before touching the shared value, release immediately after. WHY DOING EVERYTHING RIGHT WASN'T ENOUGH ------------------------------ 'disciplined' correctly waits for the lock before its own LOAD/INC/ STORE sequence, and correctly releases it afterward. But 'careless' never calls try_acquire() at all -- it reads and writes the shared physical address directly, with no idea the lock even exists. When the scheduler switches to 'careless' while 'disciplined' is legitimately holding the lock mid-increment, 'careless' simply reads and overwrites the shared value anyway -- the lock being "held" means nothing to code that never checks it. The exact same lost-update shape from Chapter 1's own Finding 1 reappears, just with only one of the two accessors being the actual cause this time. WHAT A MUTEX ACTUALLY PROTECTS ------------------------------ A mutex doesn't protect a piece of MEMORY the way a lock on a door protects a room -- there's no hardware-enforced barrier stopping 'careless' from touching mem.data[shared_paddr] directly. What a mutex actually protects is an AGREEMENT: every piece of code that touches the shared value promises to go through try_acquire()/ release() first. That agreement is a property of the CODE, not the DATA -- and it only holds if every single accessor honors it. One forgotten acquire() call anywhere in a codebase is enough to silently undo the protection everywhere else that data is used, no matter how carefully every other accessor was written. WHY THIS MATTERS BEYOND THIS ONE EXAMPLE ------------------------------ This is exactly the same shape of problem this course's own earlier chapters (and Course 1's own Chapter 6 quota gap) kept surfacing: a rule enforced correctly on SOME code paths but not others provides zero real protection for the paths that skip it, and the failure is often silent -- there's no error, no exception, nothing visibly wrong until the actual data comes out incorrect. WHY THIS WORKS AS AN ANSWER ------------------------------ Deliberately making only ONE of the two accessors correct, rather than testing either "both correct" (Finding 3, already known to work) or "both incorrect" (Chapter 1's own Finding 2, already known to fail), isolates the real question this exercise is asking: is a mutex's protection a property of the shared MEMORY itself, or a property of the CODE that agrees to use it. The measured result (real loss, despite one accessor doing everything right) answers that directly.