1. Introduction
Multithreading lets a Python program run several threads of execution concurrently within a single process, sharing the same memory space. It is most useful for I/O-bound tasks -- things like making network requests, reading/writing files, or waiting on a database -- where a thread spends most of its time waiting rather than computing. The threading module provides the Thread class along with synchronization primitives like Lock, Event, and Condition to coordinate access to shared data between threads.
Cricket analogy: During a rain-affected match, multiple ground staff work concurrently -- one covering the pitch, one clearing the outfield -- sharing the same stadium, which mirrors threads sharing one process's memory while each waits on separate tasks.
2. Syntax
import threading
def worker(name):
print(f"Thread {name} is running")
t = threading.Thread(target=worker, args=("A",))
t.start()
t.join()3. Explanation
A Thread object is created with a target callable and optional args/kwargs. Calling start() schedules the thread to begin running its target function, while join() blocks the calling thread until the target thread has finished executing -- this is essential when the main program needs to wait for background work to complete before continuing.
Cricket analogy: A team sends a substitute fielder onto the ground with start(), and the captain must wait (join()) for that fielder to return to the boundary before resuming the main game plan.
When multiple threads read and write the same shared variable without coordination, you get a race condition: the final result depends on unpredictable thread scheduling and can be wrong. Python's threading.Lock (a mutex) solves this -- a thread must acquire the lock before entering a critical section and release it afterward, guaranteeing that only one thread modifies the shared state at a time. The with lock: context manager form acquires and releases the lock automatically, even if an exception occurs.
Cricket analogy: Two scorers updating the same run total sheet at once without coordination can produce a wrong final score, so a single scorer must hold the 'pen' (lock) before writing, releasing it only after the entry is complete.
The Global Interpreter Lock (GIL) in CPython allows only one thread to execute Python bytecode at a time, even on a multi-core machine. This means threads do NOT achieve true CPU parallelism for CPU-bound work (e.g., number crunching) -- for that, use the multiprocessing module, where each process gets its own interpreter and GIL. Threading remains genuinely useful for I/O-bound work, because a thread releases the GIL while it waits on I/O, letting other threads run in the meantime.
4. Example
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock:
counter += 1
threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print("Final counter value:", counter)5. Output
Final counter value: 4000006. Key Takeaways
- threading.Thread runs a callable concurrently; start() launches it, join() waits for it to finish.
- The GIL means CPython threads never run Python bytecode truly in parallel across cores.
- Threading is ideal for I/O-bound tasks (network, disk, waiting), not CPU-bound number crunching.
- Use multiprocessing for CPU-bound parallelism -- it sidesteps the GIL entirely.
- Shared mutable state accessed by multiple threads must be protected with threading.Lock to avoid race conditions.
with lock:is the safest way to acquire/release a lock since it releases even on exceptions.
Practice what you learned
1. What does the GIL prevent in CPython?
2. Which module should you prefer for CPU-bound parallel work in Python?
3. What is the purpose of thread.join()?
4. What can happen when two threads modify a shared variable without synchronization?
5. Which construct is commonly used to protect shared data from concurrent access?
6. In the mandatory example above, why does the final counter always equal 400000?
Was this page helpful?
You May Also Like
Decorators in Python
Learn how Python decorators use the @syntax to wrap functions, adding behavior like logging or timing without changing the original function's code.
Exceptions in Python
What exceptions are, how Python's built-in exception hierarchy is structured, and how errors propagate through a program.
os Module in Python
Explore Python's os module for interacting with the operating system, including file paths, directories, and environment information.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics