Filter concepts by levelShowing all levels.

Python · Threading

Threading

threading.Thread, the coordination primitives that keep shared state correct, and the three classic ways concurrent code fails

  • threading.Threadrun a function concurrently, on the same process
  • Lock vs RLockmutual exclusion — RLock allows the SAME thread to re-acquire
  • Event, Condition, Semaphorecoordination beyond simple exclusion
  • Race, deadlock, starvationthree distinct failure modes, three distinct fixes

1.threading and Thread

threading.Thread(target=fn, args=...) creates a thread that will run fn. Nothing runs until .start() is called, and the caller can wait for it to finish with .join().

Key features

  • Thread(target=fn, args=(...)) builds a thread object; it does not run yet
  • .start() begins running fn on a separate OS thread
  • .join() blocks the caller until that thread finishes
  • Threads share the same memory as the process that created them
  • threading.current_thread().name identifies which thread is running

Thread — the methods worth knowing

Thread — the methods worth knowing
CallEffect
Thread(target=fn, args=(a,), kwargs={})builds a thread that will run fn(a, **kwargs)
.start()begins running the thread; returns immediately
.join(timeout=None)blocks the caller until the thread finishes (or timeout elapses)
.is_alive()True while the thread is still running
.daemon = Truethread is killed automatically when the main program exits

Command

python
import threading

t = threading.Thread(target=some_function, args=(arg1, arg2))
t.start()   # begins running immediately
t.join()    # blocks here until t finishes

Thread(target=fn).start() begins running immediately and returns right away; .join() is what actually waits for it to finish.

2.Lock and RLock

A Lock lets only one thread run a block of code at a time — with acquires it, others wait. An RLock is the same idea, but the SAME thread that holds it can acquire it again without blocking itself.

Key features

  • threading.Lock(): only one thread may hold it at a time
  • with lock: acquires on entry, releases on exit, even if an exception is raised
  • A plain Lock deadlocks if the SAME thread tries to acquire it twice, unreleased
  • threading.RLock(): the same thread may acquire it multiple times (reentrant)
  • An RLock tracks an internal count — it needs the same number of releases as acquires

Lock vs RLock

Lock vs RLock
PropertyLockRLock
Different threads acquiringsecond thread blocks until releasedsecond thread blocks until released
SAME thread acquiring twicedeadlocks — waits on itself foreversucceeds — internal count goes to 2
Releasingone release, fully unlockedneeds one release per acquire to fully unlock
Typical useprotecting one shared resourcea method that may call another locked method on itself
A Lock serializes access to shared state

4 threads

all want counter += 1

with lock:

only one thread inside at a time

correct total

no lost updates

  1. 4 threads — all want counter += 1
  2. with lock: — only one thread inside at a time
  3. correct total — no lost updates

Command

python
import threading

lock = threading.Lock()

def safe_increment():
    with lock:          # blocks until acquired, releases automatically
        counter += 1

with lock: makes a block run one thread at a time. A plain Lock deadlocks if the same thread re-acquires it; RLock allows that.

3.Event, Condition, and Semaphore

Event lets one thread signal "something happened" to others waiting on it. Condition lets a thread wait until some shared state becomes true. Semaphore caps how many threads may hold access at once.

Key features

  • Event: .wait() blocks until .set() is called elsewhere; .clear() resets it
  • Condition: .wait() blocks until .notify()/.notify_all() wakes it, tied to a Lock
  • Semaphore(n): only n threads may hold it at once; the n+1th call to acquire blocks
  • Condition.wait_for(predicate) re-checks the predicate automatically after each wake
  • All three build on a Lock internally — they add signalling on top of exclusion

The three coordination primitives

The three coordination primitives
PrimitivePurposeKey methods
Eventone-shot signal: "this happened"set(), clear(), wait(timeout=None), is_set()
Conditionwait until shared state satisfies a predicatewait(), wait_for(pred), notify(), notify_all()
Semaphore(n)cap concurrent access to n holders at onceacquire(), release()

Command

python
evt = threading.Event()
evt.wait()          # blocks until...
evt.set()            # ...another thread calls this

cond = threading.Condition()
with cond:
    cond.wait_for(lambda: some_flag)   # blocks until predicate is True

sem = threading.Semaphore(2)
with sem:            # blocks if 2 threads already hold it
    ...

Event: one-shot signal. Condition: wait for shared state, checked with wait_for. Semaphore(n): cap concurrent holders at n.

4.Thread safety and thread-safe data structures

A thread-safe operation stays correct when multiple threads call it at once, with no extra locking needed. queue.Queue is thread-safe; a plain list's append/pop are too — but x += 1 on a shared variable is not.

Key features

  • queue.Queue is the standard thread-safe hand-off structure between threads
  • A single list/dict method call (append, __setitem__) is atomic — CPython's GIL protects it
  • A multi-step operation (read, then write, like x += 1) is NOT atomic even in CPython
  • Thread-safe means: safe to call from multiple threads with no external locking
  • When in doubt, protect shared mutable state with a Lock rather than assuming safety

What is and is not thread-safe by default

What is and is not thread-safe by default
OperationThread-safe?
queue.Queue().put() / .get()yes — designed for cross-thread hand-off
list.append(x) / list.pop()yes — a single bytecode-level operation
dict[key] = valueyes — same reason
counter += 1 on a shared intno — read-then-write, needs a Lock
balance -= amount (any compound assignment on shared state)no — same reason

Command

python
import queue

q = queue.Queue()
q.put(item)        # safe from any thread, no lock needed
item = q.get()      # blocks until something is available

Thread-safe means the structure handles its own locking (queue.Queue, single list/dict ops). counter += 1 on shared state needs your own Lock.

5.Race conditions, deadlocks, and starvation

A race condition is losing an update when threads interleave unsafely. A deadlock is threads permanently waiting on each other's locks. Starvation is a thread that never gets the resource it needs, with no deadlock present.

Key features

  • Race condition: unsynchronized read-modify-write across threads loses updates
  • Deadlock: thread A holds lock 1 waiting on lock 2, thread B holds lock 2 waiting on lock 1
  • A consistent lock acquisition order across all threads prevents that deadlock shape
  • Starvation: a thread is repeatedly denied a resource, even with no deadlock present
  • A fair Lock/Semaphore or a timeout-based acquire are the usual starvation mitigations

The three failure modes

The three failure modes
FailureWhat happensTypical fix
Race conditionunsynchronized read-modify-write loses updatesLock (or a thread-safe structure) around the whole operation
Deadlockthreads wait on each other's locks foreveralways acquire multiple locks in the same order
Starvationa thread never gets a turn, no gridlock involvedfair scheduling, timeouts, or priority limits
Two threads, two locks, wrong order

Thread 1

  • holds lock_a
  • waits for lock_b
  • never gets it — B holds it

Thread 2

  • holds lock_b
  • waits for lock_a
  • never gets it — A holds it
  • Thread 1
    • holds lock_a
    • waits for lock_b
    • never gets it — B holds it
  • Thread 2
    • holds lock_b
    • waits for lock_a
    • never gets it — A holds it

Command

python
# race condition fix: protect the whole read-modify-write
with lock:
    counter += 1

# deadlock fix: same lock order, everywhere
with lock_a:
    with lock_b:
        ...

Race condition: lost updates from unsynchronized access. Deadlock: circular waiting on locks. Starvation: a thread perpetually denied a turn, no cycle involved.

Remember

  • thread.start() launches it; thread.join() blocks until it finishes — forgetting join() can let the program exit mid-work.
  • with lock: makes a block run one thread at a time; a plain Lock deadlocks if the SAME thread re-acquires it, RLock does not.
  • A race condition needs a genuine shared, mutable resource and a gap between reading and writing it — protect that gap.
  • A deadlock needs a cycle of threads each waiting on a resource the next one holds — always acquire multiple locks in the same order.

Pro tip

  • Prefer higher-level primitives (Queue, concurrent.futures) over raw Lock/Thread when the pattern fits — less to get wrong by hand.
  • threading.Event is the simplest coordination tool — one thread signals, others wait, no shared counter or condition logic needed.
  • Starvation is a fairness problem, not a correctness one — a thread can be technically able to proceed and still never get scheduled.
  • Most built-in mutable types (list, dict) are NOT guaranteed thread-safe for compound operations — protect them explicitly.

Next up

  • Multiprocessing

Sources

  • threading — docs.python.org/3/library/threading.html
  • Every example executed on Python 3.14.3 · checked 2026-08-21

Spotted a mistake on this page? Tell us.