At a party, everyone's phone adds songs to one shared playlist, and other parts of the app (a screen showing the queue, a DJ bot) want to hear about every new song.
Implement a thread-safe Playlist() with:
add(song) -> None: append one song.add_many(songs) -> None: append several songs as one block: no song from another thread may land in the middle of the block.songs() -> list: a copy of the playlist so far, in order.subscribe(listener) -> None: from now on, calllistener(song)once for every song added (foradd_many, once per song, in order, after the whole block is in).
Listeners are other people's code, and they do things like:
- read the playlist:
lambda song: print(len(p.songs())), - add a song themselves: the DJ bot answers
"Finale"by adding"Encore", - hand work to another thread and wait for it, and that thread calls
p.songs().
p = Playlist()
p.subscribe(lambda s: p.add("Encore") if s == "Finale" else None)
p.add("Opener")
p.add_many(["Slow one", "Finale"])
p.songs() # ["Opener", "Slow one", "Finale", "Encore"]
None of these may hang, and concurrent adds must never lose a song.
There are two deadlocks waiting here:
add_manycallingadd(or a listener callingadd/songs) while the same thread already holds a plainLock: a thread waiting for itself.- A listener that waits for another thread which needs the lock. Even an
RLockdoesn't save you: the lock is held by the thread that's waiting.
Constraints: don't sleep or busy-wait, and keep one lock per playlist.
Show hint
Never call outside code while holding a lock. In add_many, take the lock, extend the list and copy the listener list, release it, and only then call the listeners. add(song) can simply be add_many([song]).