Locks, semaphores, conditions
Six synchronisation primitives, each taught through the mistake it prevents: the bug first, forced with real threads where the code allows it, then the fix and the tests that prove it.
Why does Condition.wait have to sit in a while loop and not an if?
Because waking up tells you the lock is back, not that the thing you were waiting for is still there. A notify_all wakes every waiter, and each of them then has to take the lock again; by the time the second one holds it, the first may have taken the only item. The threading documentation says the loop is necessary because wait can return after an arbitrarily long time and the condition that prompted the notify may no longer hold. Section 04 walks through that order step by step, and the same loop also stops a consumer from waiting for a notify that already happened. Condition.wait_for(predicate) writes the loop for you and is the version to type in a round.
Is list.append thread-safe in CPython, and can I skip the lock?
It is, today, and you should still take the lock. The Python FAQ lists append, extend, x = L[i], L.pop() and a few dict operations as atomic, but that is a fact about CPython and not a rule of the language, and the free-threading HOWTO recommends a threading.Lock instead of relying on the internal locks of built-in types. The FAQ also lists i = i + 1 and D[x] = D[x] + 1 as not atomic, and almost every real use is a pair of operations rather than one. In a round the answer that scores is: yes in CPython today, and I would take the lock anyway, because the next reader of this file cannot tell which lines were safe on purpose.
When do I actually need an RLock?
When one method that holds the lock calls another method that takes the same lock. With a plain Lock the thread waits for itself for ever; section 02 shows the second acquire returning False with a timeout, where an RLock returns True. An RLock knows its owning thread and counts how many times the owner took it. Needing one is usually a sign that your public methods both guard and call each other, and the smaller fix is a private helper that assumes the lock is already held. Say both out loud: the RLock works, and the layering is what made it necessary.
Event or Condition?
An Event when the answer is a single fact that becomes true once and stays true: the service is up, the shutdown has been asked for, the setup has finished. Every waiter goes through on one set, nobody holds anything, and there is no predicate and no lock to reason about. A Condition when what you are waiting for is a state that can change back, such as a queue being non-empty or a buffer having room, because then you need the lock that guards the state and a predicate you re-check under it. Using an Event to hand work over is the classic mistake, and section 05 shows it losing an item: two sends before a receive leave one item, because a flag cannot count.
The standard library has no reader-writer lock. Should I write one in an interview?
Only when the workload is genuinely read-heavy and you can say so. Section 06 builds it from one Condition: a count of readers, a flag for the writer, and one predicate per side, with a test that forces several readers inside at once. The part that earns the marks is naming the fairness problem before you are asked. A steady stream of readers can starve a writer for ever, because a new reader only waits while somebody is writing. The fix is to make readers also wait while a writer is queued, and to have a writer that gives up wake the readers it was holding back.