Interview concept

Concurrency and threads

How threads share data safely, and the races and deadlocks to watch out for.

Coding concept

What it is

Concurrency is several things running at once and sharing data. The trouble is that "read a value, change it, write it back" isn't one step. Two threads can both read 5, both add 1, and both write 6, and you've lost an update. That's a race condition.

How it works

ToolWhat it doesUse it for
Lock (mutex)Only one thread inside at a timeProtecting a check-then-update
Condition variableSleep until another thread says something changedWaiting for space or for work
SemaphoreLets up to n threads inLimiting how many run at once
Thread-safe queueHandles all of the above for youPassing work between threads

A bounded queue is the classic interview build. Producers wait when it's full, consumers wait when it's empty, and a lock protects the list itself:

bounded_queue.pypython
import threading
from collections import deque


class BoundedQueue:
    def __init__(self, capacity):
        self.items = deque()
        self.capacity = capacity
        self.lock = threading.Lock()
        self.not_full = threading.Condition(self.lock)
        self.not_empty = threading.Condition(self.lock)

    def put(self, item):
        with self.not_full:
            while len(self.items) >= self.capacity:
                self.not_full.wait()
            self.items.append(item)
            self.not_empty.notify()

    def get(self):
        with self.not_empty:
            while not self.items:
                self.not_empty.wait()
            item = self.items.popleft()
            self.not_full.notify()
            return item

Waits are inside a while loop, not an if. A thread can wake up and find another thread got there first, so it has to check again.

Trade-offs

One big lock is simple and correct, but every thread waits on it. Finer locks, like one per key, let more run at once, but if two threads take two locks in opposite orders, each waits for the other forever. That's a deadlock. Always taking locks in the same order prevents it.

In an interview

  • Point out exactly which steps need to happen together.
  • Start with one lock, then make it finer only if you need to.
  • Mention deadlocks and how your lock order avoids them.

Worked example

A thread-safe log writer

Many threads in a service call log(message). The messages should end up in a file, in the order they were logged, without slowing down the threads that call log.

  1. Writing to the file directly from every thread means they all fight over it, and two writes can interleave halfway through a line.
  2. A lock around the write fixes interleaving, but now every caller waits for disk I/O. That's the 'slows down callers' problem.
  3. Better: log() just puts the message on a thread-safe queue and returns. A single writer thread takes messages off and writes them. Only one thread touches the file, so the file itself needs no lock.
  4. Order is preserved because the queue is first in, first out, and there's one writer.
  5. If the disk is slow and the queue grows forever, you'll run out of memory. Make it a bounded queue. When it's full, either block the caller or drop messages and count the drops, and say which you chose.
  6. The writer can batch: take everything waiting, write it in one go, and flush. That's far fewer disk writes when things are busy.
  7. On shutdown, put a special 'stop' message on the queue. The writer drains everything before it, flushes, and exits, so nothing is lost.

Follow-up questions

Things interviewers tend to ask once you've brought this up.

How do you make a counter thread-safe?
Put the read-add-write inside a lock, or use an atomic operation if your language has one. For very hot counters, give each thread its own count and add them up when asked.
What's the difference between a lock and a semaphore?
A lock lets one thread in at a time. A semaphore lets up to n in, which is useful for limiting how many requests hit a database at once.
How do you avoid deadlocks?
Always take locks in the same order, hold them as briefly as you can, and don't call code you don't control while holding one.
When would you use a read-write lock?
When reads are far more common than writes. Many readers can hold it at once, and a writer waits for them to finish.

Common mistakes

  • Checking a condition and acting on it in separate steps, without a lock around both.
  • Using 'if' instead of 'while' around a wait.
  • Holding a lock while doing slow I/O.
  • Taking two locks in different orders in different places.
  • Assuming Python's GIL makes code thread-safe.