Introduction
Python's threading module is the standard way to run tasks concurrently within a single process. Whether you are handling multiple network connections, running background tasks, or building a producer-consumer pipeline, threads are a fundamental building block. This lesson walks through creating, managing, and coordinating threads.
Key Concepts
- Thread: A lightweight unit of execution that shares memory with other threads in the same process.
- Daemon thread: A background thread that is automatically terminated when the main program exits.
- Thread-local data: Storage where each thread sees its own independent copy of a variable.
- join(): A method that blocks the calling thread until the target thread finishes.
Real World Context
A chat server needs to handle hundreds of simultaneous client connections. Each connection can be managed in its own thread, reading messages and broadcasting them. Daemon threads handle periodic tasks like logging stats, and thread-local data stores per-connection context without passing it through every function call.
Deep Dive
Creating Threads
pythonimport threading # Method 1: Pass a function def worker(name): print(f"Worker {name} starting") t = threading.Thread(target=worker, args=("A",)) t.start() t.join() # Wait for completion # Method 2: Subclass Thread class MyThread(threading.Thread): def __init__(self, name): super().__init__() self.name = name def run(self): print(f"Thread {self.name} running") t = MyThread("B") t.start()
Thread Properties and Methods
pythont = threading.Thread(target=worker) t.start() # Start the thread t.join(timeout=5) # Wait for completion t.is_alive() # Check if running t.name # Thread name t.daemon = True # Daemon thread (dies with main) threading.current_thread() # Get current thread threading.active_count() # Number of active threads threading.enumerate() # List all threads
Daemon Threads
pythonimport threading import time def background_task(): while True: print("Background work...") time.sleep(1) # Daemon thread dies when main program exits t = threading.Thread(target=background_task, daemon=True) t.start() print("Main program continues...") time.sleep(3) print("Main program exits, daemon dies")
Thread-Local Data
pythonimport threading # Each thread gets its own copy local_data = threading.local() def worker(value): local_data.x = value # Thread-specific print(f"Thread sees x = {local_data.x}") threads = [threading.Thread(target=worker, args=(i,)) for i in range(3)] for t in threads: t.start() for t in threads: t.join()
Common Pitfalls
- Forgetting to call join() — If the main thread exits before worker threads finish, daemon threads are killed immediately and non-daemon threads keep the process alive unexpectedly. Always join threads you care about.
- Subclassing Thread without calling super().init() — Forgetting to call the parent constructor in a Thread subclass causes cryptic errors when you try to start the thread.
- Sharing mutable objects without locks — Threads share memory. Without synchronization, concurrent writes to the same data structure produce race conditions.
Best Practices
- Prefer passing functions over subclassing Thread — Using
Thread(target=fn)is simpler and more Pythonic. Reserve subclassing for cases where you need to store complex per-thread state. - Use daemon threads for background work that can be interrupted — Daemon threads are ideal for logging, heartbeats, or monitoring tasks that should stop when the main program ends.
Summary
- Create threads by passing a callable to
threading.Thread(target=fn)or by subclassingThread. - Use
join()to wait for a thread to complete anddaemon=Truefor background threads. - Thread-local data gives each thread its own isolated copy of a variable.
- Always synchronize access to shared mutable state with locks.
Code Examples
python
import threading
from queue import Queue
# Producer-Consumer pattern
def producer(queue):
for i in range(5):
queue.put(i)
print(f"Produced {i}")
def consumer(queue):
while True:
item = queue.get()
if item is None:
break
print(f"Consumed {item}")
queue.task_done()
q = Queue()
p = threading.Thread(target=producer, args=(q,))
c = threading.Thread(target=consumer, args=(q,))
p.start(); c.start()
p.join()
q.put(None) # Signal consumer to stop
c.join()