~/problems / Locks / Deadlock and lock ordering

Shared party playlist

easy ~15 min

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, call listener(song) once for every song added (for add_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:

  1. add_many calling add (or a listener calling add/songs) while the same thread already holds a plain Lock: a thread waiting for itself.
  2. A listener that waits for another thread which needs the lock. Even an RLock doesn'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]).

Topic: Deadlock and lock ordering. The four conditions and how to break one.

0:00
Ctrl ' run · Ctrl ↵ submit
esc