Home/Python/Threads and locks

Threads and locks

Share memory safely and wait for work to finish

Threads

Threads are independently scheduled execution paths within a process. They share Python objects and can overlap blocking I/O, but their operations can interleave.

Create Thread(target=function, args=(value,)), call start(), then join() to wait for completion. Calling run() directly stays in the current thread. Start workers before joining them to allow overlap; a thread object can start only once.

Python
from threading import Lock, Thread

numbers = []
lock = Lock()

def record(value):
    with lock:
        numbers.append(value)

threads = [Thread(target=record, args=(n,)) for n in (1, 2)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()
print(sorted(numbers))  # [1, 2]

Joining ensures both workers have finished. Their append order is unspecified; sorting stabilizes the displayed result, not execution order.

Critical sections

A lock protects an invariant across related operations. Keep a check and its update in one critical section:

Python
from threading import Lock, Thread

remaining = 3
lock = Lock()
def reserve_two():
    global remaining
    with lock:
        if remaining >= 2:
            remaining -= 2

threads = [Thread(target=reserve_two) for _ in range(2)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()
print(remaining)  # 1

Only one reservation can pass. All readers and writers relying on this state must follow the same locking rule. In an application, an owning object can hold both state and lock.

Use with lock to release the lock after ordinary exceptions. Locking does not roll back earlier mutations if a later step fails. Validate first or define rollback, and return snapshots when callers should not mutate shared containers.

Gotchas

  • In GIL-enabled CPython, the GIL limits simultaneous Python-bytecode execution; it does not make business operations atomic. Some libraries release it, and free-threaded builds have different behavior. Synchronize shared state explicitly.
  • Avoid blocking I/O and unknown callbacks while holding a lock. Consistent lock acquisition order helps prevent deadlocks; RLock permits reentry but does not solve arbitrary lock-order cycles.
  • join() waits for one worker, not every writer, and does not re-raise that worker's exception. Futures provide a result/failure channel.
  • More threads can increase overhead and overload dependencies. Daemon threads can stop abruptly, so important cleanup needs explicit ownership and joins.

This lesson requires native Python; the browser runtime does not provide ordinary operating-system threads.

Put it into practice

Implement the exercise in the workspace, then download it and run the tests with your local toolchain.

Open practice
Next lessonThread-safe queues