๐ŸงตThread, Lock, RLock, race conditions, GILLESSON

Threading in Python

Python's threading module lets you run multiple tasks concurrently within a single process. Understanding threads โ€” and the Global Interpreter Lock that governs them โ€” is essential for writing responsive I/O-bound programs.

Creating and Starting Threads

A thread is created by instantiating threading.Thread with a target function. Call .start() to launch it and .join() to wait for it to finish:

The key insight: both threads run simultaneously. While thread A sleeps for 2 seconds, thread B completes its 1-second sleep and finishes first.

Passing Arguments

Use args= for positional arguments and kwargs= for keyword arguments:

Daemon Threads

A daemon thread runs in the background and is automatically killed when all non-daemon threads (including the main thread) exit. Use them for background tasks like monitoring or cleanup that shouldn't prevent program shutdown:

Set daemon=True before calling .start(), or use thread.daemon = True.

Race Conditions

A race condition happens when two threads read and write shared data simultaneously, producing unpredictable results:

The problem: counter += 1 compiles to three bytecode operations. A thread switch can happen between any two of them, causing one thread to overwrite the other's update.

threading.Lock โ€” Mutual Exclusion

A Lock (mutex) ensures only one thread executes a critical section at a time:

Always use with lock: rather than manually calling lock.acquire() / lock.release() โ€” the context manager guarantees release even if an exception occurs.

threading.RLock โ€” Reentrant Locks

A regular Lock deadlocks if the same thread tries to acquire it twice. RLock (reentrant lock) allows the same thread to acquire it multiple times:

Use RLock when you have recursive functions or methods that all need the same lock.

The GIL โ€” Global Interpreter Lock

CPython (the standard Python interpreter) has a Global Interpreter Lock โ€” a mutex that allows only one thread to execute Python bytecode at a time, even on multi-core machines.

What this means in practice:

  • I/O-bound tasks: Threads work great. When a thread waits for network/disk I/O, it releases the GIL, letting other threads run. Web scraping, file processing, database queries โ€” threading speeds these up.
  • CPU-bound tasks: Threads don't help. All threads compete for the single GIL, so you get no parallelism. For CPU-heavy work, use multiprocessing instead.

threading.Event โ€” Signaling Between Threads

Event is a simple communication primitive: one thread signals an event, others wait for it:

threading.Semaphore โ€” Limiting Concurrency

A Semaphore limits how many threads can access a resource simultaneously. It's like a lock with a counter:

Thread-Local Storage

Each thread can have its own copy of a variable using threading.local():

Quick Reference

PrimitivePurpose
Thread(target, args)Create a thread
.start() / .join()Launch / wait for thread
daemon=TrueAuto-kill on main exit
LockMutual exclusion (one at a time)
RLockReentrant mutual exclusion
EventSignal/wait between threads
Semaphore(n)Limit n concurrent accesses
local()Per-thread storage

The GIL means Python threads are best suited for I/O-bound tasks. For CPU parallelism, reach for multiprocessing or concurrent.futures.ProcessPoolExecutor.

Knowledge Check

What is a race condition in multithreaded programming?

The Global Interpreter Lock (GIL) means that Python threads are most useful for which type of tasks?

What is the difference between threading.Lock and threading.RLock?