Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 14 – Concurrency and parallelism: threads, processes, interpreters

Thread safety: locks, events and queues

Protect shared data with Lock and RLock, signal between threads with Event and Semaphore, and hand work to worker threads through a queue.Queue.

  • Advanced
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: A lock makes the update one step · Events, semaphores and conditions make threads wait for each other · A queue hands work over and does the locking itself · Check yourself. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Protect shared state with Lock and RLock
  • Use Event, Semaphore and Condition to coordinate threads
  • Build a producer-consumer pipeline with queue.Queue and shut it down cleanly
  • Reproduce overselling and a deadlock, and prevent both
  • Limit a pipeline's memory with a queue's maxsize, and stop its workers on request

Before you start

What you’ll learn · Before you start

What you will learn

  • Protect shared state with Lock and RLock
  • Use Event, Semaphore and Condition to coordinate threads
  • Build a producer-consumer pipeline with queue.Queue and shut it down cleanly
  • Reproduce overselling and a deadlock, and prevent both
  • Limit a pipeline's memory with a queue's maxsize, and stop its workers on request

Before you start

On this page

Four threads make 250 deposits of 10 each into one wallet, so the balance should end at 10,000. Without protection most of the money vanishes, because each thread reads the balance, waits a moment and writes back a sum built on an old value. A lock fixes it: with lock: around the read, the change and the write lets only one thread at a time run them, so they act as one step.

Other tools make threads wait for each other. An event is a flag that one thread sets and others wait for, a semaphore lets at most a fixed number of threads past at once, and a condition wakes a waiting thread after another one changes shared data. A queue, queue.Queue, hands work from producer threads to consumer threads and does the locking itself, so threads exchange work instead of sharing data.

A lock makes the update one step

Each deposit below waits 0.1 milliseconds between its read and its write. The program makes the 1,000 deposits twice, without a lock and with one:

1,000 deposits from four threads, without a lock and with one Python · wallet_lock.py
import threading
import time


class Wallet:
    def __init__(self):
        self.balance = 0
        self._lock = threading.Lock()

    def deposit_unsafely(self, amount):
        current = self.balance
        time.sleep(0.0001)  # a short wait between the read and the write
        self.balance = current + amount

    def deposit(self, amount):
        with self._lock:  # one thread at a time from here ...
            current = self.balance
            time.sleep(0.0001)  # the same wait, now inside the lock
            self.balance = current + amount  # ... to here


def run(locked):
    """Four threads make 250 deposits of 10 each into one new wallet."""
    wallet = Wallet()
    deposit = wallet.deposit if locked else wallet.deposit_unsafely

    def make_deposits():
        for _ in range(250):
            deposit(10)

    start = time.perf_counter()
    threads = [threading.Thread(target=make_deposits) for _ in range(4)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
    return wallet.balance, time.perf_counter() - start


balance, _ = run(locked=False)
print(f"without a lock: balance {balance:,} after 1,000 deposits of 10 (expected 10,000)")
balance, took = run(locked=True)
print(f"with a lock:    balance {balance:,} after 1,000 deposits of 10 (expected 10,000)")
print(f"the locked run took {took:.2f} s: its waits ran one at a time")

Output

without a lock: balance 2,510 after 1,000 deposits of 10 (expected 10,000)
with a lock:    balance 10,000 after 1,000 deposits of 10 (expected 10,000)
the locked run took 0.23 s: its waits ran one at a time

This output changes from run to run: how many deposits survive without the lock depends on how the threads happen to be scheduled, and the time is measured on each run; the locked balance is the same every time

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 wallet_lock.py

Without the lock about a quarter of the money arrived, a different amount on every run; with it, the balance is right. with self._lock: calls acquire() when the block starts and release() when it ends, however it ends, as a try and finally would, and every other thread that reaches the same with waits meanwhile. Step through two deposits of 10 and watch when thread B gets to read the balance:

Interactive · message by message Two deposits of 10, without a lock and with one Step through two threads that deposit 10 into the same wallet. Watch what each thread reads, and when thread B has to wait.
Two deposits of 10, without a lock and with one, step 12 of 12. Messages so far: the wallet to thread A: reads balance 0; the wallet to thread B: reads balance 0; thread A to the wallet: balance = 0 + 10; thread B to the wallet: balance = 0 + 10; thread A to the wallet: acquire(): taken; the wallet to thread A: reads balance 0; thread B to the wallet: acquire(): waits; thread A to the wallet: balance = 10, release(); the wallet to thread B: reads balance 10; thread B to the wallet: balance = 20, release().thread Adonethe walletbalance 20, lock freethread Bdonereads balance 0reads balance 0balance = 0 + 10balance = 0 + 10acquire(): takenreads balance 0acquire(): waitsbalance = 10, release()reads balance 10balance = 20, release()

The drawing shows the whole scene; its 12 steps are listed below.

Every step as text
  1. Two deposits, no lock. Threads A and B each deposit 10 into a wallet whose balance is 0, with deposit_unsafely(). Each deposit reads the balance, waits a moment and writes back the sum.
  2. A reads 0. Thread A reads the balance, 0, and starts its short wait. A thread that waits lets the other threads run.
  3. B reads 0 too. Thread B runs during A's wait and reads the balance. A has not written yet, so B reads the same 0.
  4. A writes 10. A's wait ends and it writes its sum, 0 + 10.
  5. B overwrites it. B writes its own sum, also built on the 0 it read, so the balance stays 10 and A's deposit is gone. Nothing raised an error.
  6. Now with a lock. The same two deposits through deposit(). Its read and its write sit inside with self._lock, which a thread takes if it is free and waits for if another thread holds it.
  7. A takes the lock. A reaches the with statement first. The lock is free, so acquire() takes it at once.
  8. A reads 0. A reads the balance, 0, and starts its wait, this time while it holds the lock.
  9. B has to wait. B reaches the same with statement while A holds the lock, so its acquire() blocks. B cannot read the balance until A has finished.
  10. A writes and releases. A writes 0 + 10 and leaves the with block, which releases the lock however the block ends.
  11. B goes next. B's acquire() returns now. It reads the balance A has just written, 10.
  12. Both deposits counted. B writes 10 + 10 and releases the lock. Both deposits arrived, because B read only after A had written; the price is that B waited for the whole of A's update, wait included.

The locked run was slower: the waits ran one after another, because each sat inside the lock. Two rules follow.

  • Keep the locked block short. Read, decide and write inside it; do slow work, such as a network call, before or after it.
  • Never call code you do not control while you hold a lock. A callback may be slow, may wait for another thread, or may try to take the same lock.

One method that skips the lock

A lock protects only the code that takes it. A withdraw() that changes self.balance without with self._lock: can run in the middle of a deposit, and the lost update is back.

A plain Lock cannot be taken twice by one thread: a second acquire() from its holder waits for itself for ever, a A state in which threads wait for each other for ever, such as two threads that each hold one lock and wait for the other's. Nothing fails and nothing runs, so the program simply stops moving.. An A lock, threading.RLock(), that the thread holding it may take again without waiting. It counts the acquires, and only its owner may release it, once for each acquire. may be taken again by the thread that holds it, which suits locked methods that call other locked methods of the same object.

Key idea

A lock turns a read, a change and a write into one step that other threads wait for: keep it short, and take the same lock everywhere the data changes.

Checkpoint

Check: A lock around a read, a change and a write

  1. Question 1 of 2 Two threads each deposit 10 into a wallet whose balance is 0, with deposit_unsafely(), and both read the balance before either one writes. What is the balance afterwards?

    Type a number.

    A hint

    Each thread writes back the value it read plus 10. What did each one read?

    Show the answer: Question 1 of 2

    Answer: 10 in the wallet

    Both threads read 0, so both write 0 + 10. The second write replaces the first, and one deposit of 10 is lost without any error, the same thing that happened to three quarters of the money in the unlocked run.

  2. Question 2 of 2 Every deposit() takes self._lock, but a new withdraw() method changes self.balance without taking it. What can happen?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A withdrawal can run between a deposit's read and its write, and one of the two updates is lost

    A lock protects only the code that takes it. Every read, change and write of the balance has to go through the same with self._lock:, or the lost update comes back through the one path that skips it.

Events, semaphores and conditions make threads wait for each other

Locks protect data; the other tools hold threads back until something has happened. This program starts six download threads that wait for a start signal and then download at most two at a time:

Six downloads that wait for a start signal, two at a time Python · signals.py
import threading
import time

ready = threading.Event()  # a flag: False until set()
slots = threading.Semaphore(2)  # at most 2 threads inside at once
inside = most_inside = finished = 0
counter_lock = threading.Lock()


def downloader():
    global inside, most_inside, finished
    ready.wait()  # every thread waits here until the main thread sets the flag
    with slots:
        with counter_lock:
            inside += 1
            most_inside = max(most_inside, inside)
        time.sleep(0.05)  # a download
        with counter_lock:
            inside -= 1
            finished += 1


threads = [threading.Thread(target=downloader) for _ in range(6)]
for thread in threads:
    thread.start()
time.sleep(0.1)
print(f"before set(): flag {ready.is_set()}, downloads finished: {finished} of 6")
ready.set()
for thread in threads:
    thread.join()
print(f"after set(): flag {ready.is_set()}, downloads finished: {finished} of 6, most at once: {most_inside}")

Output

before set(): flag False, downloads finished: 0 of 6
after set(): flag True, downloads finished: 6 of 6, most at once: 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 signals.py

Step through the same run, from the six blocked threads to the last download:

Interactive · message by message Six downloads, one start signal, two at a time Step through signals.py. The flag holds every thread back until set(); then the semaphore lets two through at a time.
Six downloads, one start signal, two at a time, step 6 of 6. Messages so far: main thread to six downloaders: start() six times; main thread to ready and slots: ready.set(); ready and slots to six downloaders: 2 permits taken; six downloaders to ready and slots: release() after 50 ms; six downloaders to main thread: join() returns.main threadfinished 6, most at once 2ready and slotsflag True, 0 permitssix downloadersall six finishedstart() six timesready.set()2 permits takenrelease() after 50 msjoin() returns

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Six threads start. The main thread starts six downloader threads. Each one calls ready.wait() first, and the flag starts out false, so all six block there.
  2. Nothing moves yet. The main thread sleeps for 0.1 s and then counts. No thread has got past wait(), so no download has finished.
  3. The flag is set. The main thread calls ready.set(). Every thread waiting on the event wakes at once, and any later wait() returns at once too.
  4. Two permits handed out. All six threads reach with slots, but the semaphore has only two permits. Two threads take them and start downloading; the other four wait inside acquire().
  5. Permits come back. A download takes 50 ms. When a thread leaves its with slots block, its permit goes back and one of the waiting threads takes it, so two downloads are always in progress.
  6. Six of six. After three rounds of two, every download has finished. The main thread's join() calls return, and the busiest moment had two threads inside, as the program printed.

All six waited at ready.wait() until set(), and the busiest moment had two downloads in progress, one per permit of the semaphore.

Tools that make threads wait, in the threading module
Criterion LockRLockEventSemaphoreCondition
What it does one thread at a timea lock its holder may take againa flag: set(), clear(), wait()a counter of permitswaits until another thread calls notify()
Typical use a read-change-write on shared datalocked methods calling each otherstart now, stop now, config loadedat most n downloads or connectionswait until a shared value changes
Released by any threadonly the thread that took itnothing to releaseany threadits lock, while it waits
When to choose Protecting dataRe-entrant codeOne-off signalsLimits on concurrencyCustom waiting rules

An Event starts out false: wait() blocks until another thread calls set(), or until its timeout runs out. A A counter of permits, such as threading.Semaphore(2): each acquire() takes a permit and waits while none is left, and each release() gives one back, so at most that many threads are inside at once. holds a number of permits; each acquire() takes one and waits when none is left, and a BoundedSemaphore raises ValueError if code releases more permits than it took. A Condition pairs a lock with a place to wait: wait() releases the lock and sleeps, and notify(), called by the thread that changed the data, wakes a sleeper, which takes the lock back before wait() returns. Few programs need one directly, because the most common thing to wait for is new work, which a queue already handles.

Checkpoint

Check: Events, semaphores and conditions

  1. Question 1 of 2 A crawler must never have more than 4 downloads in progress, however many threads it runs. Which tool fits?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: threading.Semaphore(4), taken around each download

    A semaphore holds a counter of permits: each acquire() takes one and waits when none is left, and each release() gives one back. With 4 permits, at most 4 downloads run at any moment.

  2. Question 2 of 2 In signals.py, what are the six downloader threads doing between their start() and the main thread's ready.set()?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Each one is blocked in ready.wait()

    An Event is a flag that starts out false. Every thread that calls wait() blocks until another thread calls set(); then all of them wake at once, and the semaphore decides how many may go on.

A queue hands work over and does the locking itself

The safest shared data is data that only one thread touches. In the A design in which producer threads put work into a queue and consumer threads take it out, so each item goes to one consumer and the threads share the queue instead of the data. pattern, producer threads put() work into a queue.Queue, consumer threads get() it out, and the queue does all the locking:

Flow: a producer puts orders into a queue of at most 3, two packer threads get them, task_done and join count them, shutdown ends.Producer threadorders.put(order)queue.Queue(maxsize=3)holds at most 3 ordersConsumer threadstask_done() for each order;join() returns when all are doneshutdown(): get() raisesShutDown once the queue is emptypacker-1orders.get()packer-2orders.get()put() waits while fullget() waits while empty

A producer, a bounded queue and two consumers

Text description of the diagram

1. The producer thread puts each order into a queue.Queue created with maxsize=3. When three orders are already waiting, put() blocks until a consumer has taken one, so the producer can never run far ahead. 2. Two consumer threads, packer-1 and packer-2, each call get() in a loop. get() blocks while the queue is empty and hands every order to exactly one consumer. 3. After finishing an order, a consumer calls task_done(). The queue's join() returns once task_done() has been called for every order that was put in. 4. shutdown() closes the queue: further put() calls raise ShutDown, and a consumer waiting in get() raises ShutDown once the queue is empty, which ends its loop.

Eight orders, a queue of three and two packers Python · pipeline.py
import queue
import threading
import time

orders = queue.Queue(maxsize=3)  # put() waits while 3 orders are already queued
packed = []
packed_lock = threading.Lock()


def packer(name):
    while True:
        try:
            order = orders.get()  # waits for an order; raises ShutDown once the queue is shut down and empty
        except queue.ShutDown:
            return
        time.sleep(0.02)  # packing takes a while
        with packed_lock:
            packed.append((order, name))
        orders.task_done()  # tells join() this order is finished


workers = [threading.Thread(target=packer, args=(f"packer-{n}",)) for n in (1, 2)]
for worker in workers:
    worker.start()

for number in range(1, 9):
    orders.put(f"order-{number}")  # the producer: here, the main thread

orders.join()  # waits until every order has been marked done
orders.shutdown()  # no more orders: packers waiting in get() now raise ShutDown
for worker in workers:
    worker.join()

print(f"packed {len(packed)} orders: {', '.join(sorted(order for order, _ in packed))}")
print(f"both packers worked: {len({name for _, name in packed}) == 2}")
try:
    orders.put("order-9")
except queue.ShutDown:
    print("a put() after shutdown() raises queue.ShutDown")

Output

packed 8 orders: order-1, order-2, order-3, order-4, order-5, order-6, order-7, order-8
both packers worked: True
a put() after shutdown() raises queue.ShutDown

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pipeline.py

Step through the run and watch the queue do the waiting for both sides:

Interactive · message by message Eight orders through a queue of three Step through pipeline.py. The main thread puts orders in, two packers take them out, and the queue does the waiting for both sides.
Eight orders through a queue of three, step 10 of 10. Messages so far: main thread to orders: put order-1, order-2; main thread to orders: put order-3 to order-5; main thread to orders: put(order-6) waits; two packers to orders: task_done(), get(); main thread to orders: orders.join(); main thread to orders: orders.shutdown(); orders to two packers: get() raises ShutDown; main thread to orders: put(order-9).main threadjoin() returnedordersshut downtwo packersboth loops endedput order-1, order-2put order-3 to order-5put(order-6) waitstask_done(), get()orders.join()orders.shutdown()get() raises ShutDownput(order-9)raises queue.ShutDown

The drawing shows the whole scene; its 10 steps are listed below.

Every step as text
  1. The packers wait. Two packer threads start and call orders.get(). The queue is empty, so get() blocks in both of them until an order arrives.
  2. The first orders. The main thread puts order-1 and order-2. Each get() that was waiting takes one of them, so both packers start packing at once.
  3. The queue fills up. Packing takes 20 ms, and the main thread keeps putting orders. With both packers busy, order-3, order-4 and order-5 wait in the queue, which is now full.
  4. put() waits. The next put() blocks, because the queue already holds its maxsize of three orders. The producer cannot run ahead of the packers.
  5. A packer finishes one. A packer finishes an order, calls task_done() and takes the next one with get(). That frees a place, so the waiting put() goes through.
  6. join() waits for the count. All eight orders are in. orders.join() blocks until task_done() has been called once for every order that was put in, packed or not yet.
  7. Eight done. The packers mark the last orders done and join() returns. The queue is empty again, and each order went to exactly one packer.
  8. shutdown(). The main thread calls orders.shutdown(). The queue accepts nothing more, and a get() on a shut-down, empty queue raises queue.ShutDown.
  9. The packers stop. Both packers were waiting in get(), so both get queue.ShutDown, leave their loops and end. No sentinel and no daemon thread were needed.
  10. A late put() fails. The program tries one more put(order-9) after shutdown() and gets queue.ShutDown, so no order can slip into a queue that nobody reads.

  • Queue(maxsize=3) holds at most three orders, so put() waited whenever the packers fell behind. Without a maxsize the queue grows without limit.
  • Each packer called task_done() after an order, and orders.join() returned only when all eight were done.
  • shutdown(), new in Python 3.13, then closed the queue: the packers waiting in get() raised queue.ShutDown and left their loops, and the late put() raised it too.

Each order went to exactly one packer, so the only lock in the program guards the shared results list.

Older programs end their consumers with a sentinel: A value that marks the end of the data instead of a count, such as the b"" that read() returns at the end of a file. iter(func, sentinel) calls func() until it returns that value. More in “How iteration works: iterables and iterators”, a value such as None that means “no more work”. shutdown(immediate=True) drops the orders still queued, so the consumers’ next get() raises at once, and daemon threads simply stop when the program exits.

Checkpoint

Check: Handing work over with a queue

  1. Question 1 of 2 In pipeline.py both packers are busy with an order each and the queue is empty. How many more orders can the main thread put() before a put() has to wait?

    Type a number.

    A hint

    The queue was created as Queue(maxsize=3). The orders being packed are no longer in it.

    Show the answer: Question 1 of 2

    Answer: 3 orders

    The packers have already taken their orders out of the queue, so all three places are free. Three orders go in, the queue is full, and the fourth put() waits until a packer takes one out.

  2. Question 2 of 2 What does orders.join() wait for?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: For task_done() to have been called once for every order that was put in

    Queue.join() blocks until the count of unfinished items reaches zero. Each put() adds one and each task_done() removes one, so a consumer that forgets task_done() leaves join() waiting for ever.

Check yourself

Practise the three ideas above, locks, signals and queues, with questions that start where your answers so far were weakest.

Practice that adapts to you

Practise this lesson

10 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 What does with lock: around a block of code guarantee?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: Only one thread at a time runs the block, and the lock is released however the block ends, an exception included

    The with statement calls acquire() on entry and release() on exit, so the lock is never left taken by an exception. It protects only code that takes the same lock: other code that touches the same data without the lock is still unprotected.

  2. Question 2 of 10 In wallet_lock.py four threads make 1,000 deposits that each wait 0.1 ms. Spread over four threads that could take about 25 ms, yet the locked run takes far longer. Why?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: The wait sits inside the lock, so the four threads take turns and the waits run one after another

    While one thread holds the lock, the other three wait for it, so every wait happens alone: 1,000 of them in a row, plus the time each sleep really takes. That is the price of a lock. Keep slow work, such as a payment or a network call, outside the locked block.

  3. Question 3 of 10 transfer() takes self._lock and then calls withdraw(), which takes self._lock too. With a plain Lock the program hangs on the first transfer. What is the fix?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: Use an RLock, which the thread that holds it may take again

    A plain Lock does not know which thread holds it, so the thread that holds it waits for itself, a deadlock. An RLock remembers its owner and counts the acquires; another way out is a private helper that does the work and expects its caller to hold the lock.

  4. Question 4 of 10 What does ready.wait() do in a thread that calls it after ready.set() has already been called?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: It returns True at once

    An Event remembers its state. Threads that call wait() before set() block; threads that call it afterwards go straight through, until someone calls clear().

  5. Question 5 of 10 Worker threads must not start their work until the main thread has loaded the configuration, once. Which tool fits?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: An Event that the main thread sets after loading, and that every worker waits for

    An Event is the tool for "go once this has happened": start now, stop now, the configuration is loaded. It needs no lock and no counter.

  6. Question 6 of 10 Consumer threads must wake up whenever a new job lands in a shared list. A colleague proposes a Condition with wait() and notify(). What would you suggest instead?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: A queue.Queue, whose get() already waits for new items and does the locking

    A Condition can do it, but queue.Queue already pairs a lock with waiting for new items. Use a Condition only for a waiting rule a queue cannot express.

  7. Question 7 of 10 What does pipeline.py print?

    What does this program print? Choose one answer.

    import queue
    import threading
    import time
    
    orders = queue.Queue(maxsize=3)  # put() waits while 3 orders are already queued
    packed = []
    packed_lock = threading.Lock()
    
    
    def packer(name):
        while True:
            try:
                order = orders.get()  # waits for an order; raises ShutDown once the queue is shut down and empty
            except queue.ShutDown:
                return
            time.sleep(0.02)  # packing takes a while
            with packed_lock:
                packed.append((order, name))
            orders.task_done()  # tells join() this order is finished
    
    
    workers = [threading.Thread(target=packer, args=(f"packer-{n}",)) for n in (1, 2)]
    for worker in workers:
        worker.start()
    
    for number in range(1, 9):
        orders.put(f"order-{number}")  # the producer: here, the main thread
    
    orders.join()  # waits until every order has been marked done
    orders.shutdown()  # no more orders: packers waiting in get() now raise ShutDown
    for worker in workers:
        worker.join()
    
    print(f"packed {len(packed)} orders: {', '.join(sorted(order for order, _ in packed))}")
    print(f"both packers worked: {len({name for _, name in packed}) == 2}")
    try:
        orders.put("order-9")
    except queue.ShutDown:
        print("a put() after shutdown() raises queue.ShutDown")
    Show the answer: Question 7 of 10

    Answer: it prints

    packed 8 orders: order-1, order-2, order-3, order-4, order-5, order-6, order-7, order-8
    both packers worked: True
    a put() after shutdown() raises queue.ShutDown

    The main thread put all eight orders, waiting whenever three were queued; join() returned after eight task_done() calls, shutdown() ended both packers, and the late put() raised queue.ShutDown.

  8. Question 8 of 10 What does a queue.Queue() created without maxsize do when its producers are faster than its consumers?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: It keeps growing, because put() never waits

    maxsize=0, the default, means the queue has no limit. Give a queue between threads a maxsize so that a fast producer waits instead of filling the memory.

  9. Question 9 of 10 A consumer takes every item with get() but never calls task_done(). What happens to the producer's q.join()?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: It never returns, because the count of unfinished items never reaches zero

    put() adds one to the count of unfinished items and task_done() removes one. Without task_done() the count stays above zero, so join() waits for ever, which looks like a hang at the end of the program.

  10. Question 10 of 10 A consumer thread loops on orders.get(). After orders.join() returns, the main thread calls orders.shutdown(). What happens to the consumer?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: Its waiting get() raises queue.ShutDown, because the queue is shut down and empty, so its loop can end

    Since Python 3.13, shutdown() unblocks every thread waiting in get() once the queue is empty, and they raise queue.ShutDown. Catching it is a clean way to end a consumer's loop. Before 3.13, programs put a sentinel value, often None, into the queue instead.

A seat counter that cannot oversell, and a deadlock reproduced Pro: opens with a Pro, Premium or Ultimate pass

Overselling is the race that costs real money, and a deadlock is the bug that careless locking adds in its place. This part oversells seats on purpose, fixes it with the payment outside the lock, reproduces a deadlock between two locks with a time limit so that the program does not hang, and ends with an exercise.

In this part:

  • Forty passengers, five seats: overselling reproduced and fixed
  • Two locks in opposite orders: a deadlock reproduced
  • Exercise: a seat inventory that can’t oversell
    • A seat inventory that can't oversell

2 diagrams · 2 runnable examples (Python) · 1 exercise with sample tests · about 11 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Bounded, stoppable pipelines, with the numbers measured Pro: opens with a Pro, Premium or Ultimate pass

A queue between threads decides how much memory a pipeline may use, and an event decides how quickly it can stop. This part measures what a bounded queue does to a fast producer, stops a worker on request in a fraction of a millisecond, and compares the four ways to end a pipeline.

In this part:

  • Backpressure measured: a fast producer, a slow consumer
  • A stop event ends a worker without waiting out its pause
  • Four ways to end a pipeline, compared

2 runnable examples (Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What is the difference between threading.Lock and threading.RLock, and when do you need an RLock?

    A Lock is either locked or unlocked, and it does not remember who locked it: once any thread holds it, every further acquire() waits, even from the thread that already holds it, and any thread may release it. An RLock, a reentrant lock, has an owner and a count: the thread that holds it may acquire it again without waiting, it must release it once for each acquire, and only the owner may release it. You need an RLock when code that holds a lock calls other code that takes the same lock, typically one locked method of a class calling another. With a plain Lock that call would wait for itself forever. When no such nesting exists, a plain Lock is simpler, and an accidental nested acquire shows up at once, as a hang, instead of passing unnoticed.

  2. Mid to senior

    A booking service runs on a thread pool. Its code checks if seats_left > 0, charges the card, then decrements seats_left. During a burst it sold 3 more tickets than the train had seats. Explain the bug, fix it, and say what changes when the count lives in a database.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Senior to staff

    A service that uses two locks freezes about once a week under load: no CPU use, no errors, requests just stop. How do you confirm that it is a deadlock, find the two code paths involved, and prevent it from coming back?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    Design a pipeline that resizes 80,000 product photos of about 3 MB each: one thread reads files, three threads resize, one thread writes the results. How do you bound memory, report failed photos, and stop cleanly on Ctrl+C?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Code inside with lock: runs in one thread at a time, so a read, a change and a write inside it lose no update.
  • Keep locked blocks short, take the same lock wherever the data changes, and call no outside code while holding it.
  • An Event is a flag threads wait for, a Semaphore(n) lets n threads past at once, and a Condition wakes a waiter after a change.
  • A queue.Queue hands work over and does the locking: maxsize bounds it, task_done() and join() count finished work, and shutdown() ends the consumers.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.