Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to our third module, "Python Concurrency and Parallelism."
In our last lesson, we built a thread-safe LFU cache using a global lock. We concluded that while this coarse-grained lock ensures correctness, it serializes all cache operations, creating a performance bottleneck. This limitation isn't just a design choice; it's deeply connected to how Python's most common implementation, CPython, handles threads. This brings us directly to the topic of today's lesson.
Your background in building high-throughput systems like Rudi in Rust has given you first-hand experience with true, OS-level parallelism. In Rust, you can spawn multiple threads and expect them to run across all available CPU cores. In Python, the story is more complex due to a mechanism called the Global Interpreter Lock (GIL).
This lesson will demystify the GIL and its practical consequences. Our goal is to explain the impact of the GIL and determine when to use threading, multiprocessing, or asyncio. By the end, you'll have a clear framework for choosing the right concurrency model for different problems in Python.
1. What is the Global Interpreter Lock (GIL)?
At its core, the GIL is a mutex (a mutual exclusion lock) that protects access to Python objects, preventing multiple threads from executing Python bytecode at the same time within a single process. Even on a multi-core processor, only one thread can hold the GIL and execute Python code.
To understand why it exists, let's watch a short video.
The GIL Is DEAD: Python's Is NOW Multithreaded
The video "The GIL Is DEAD" from The Coding Gopher provides an excellent, concise explanation of what the GIL is and its historical context.
Please watch from the beginning until 01:04. Focus on the definition of the GIL as a mutex and the reason for its existence: protecting CPython's reference-counting memory management system.
As the video explains, the GIL was a pragmatic solution in Python's early days to make memory management thread-safe without requiring complex fine-grained locks on all internal data structures. However, this simplicity comes with a significant trade-off.
2. The Impact of the GIL: CPU-Bound vs. I/O-Bound Tasks
The GIL's most significant impact is that it effectively prevents true parallelism for multi-threaded Python programs. If you have a CPU-intensive task (like a heavy computation), creating multiple threads will not make it run faster. In fact, it will likely run slightly slower due to the overhead of managing the threads.
The threads will contend for the GIL, leading to a situation where only one is making progress at any given moment.

However, there's a crucial exception: the GIL is released by a thread when it's waiting for I/O (Input/Output) operations, such as reading a file from a disk, making a network request, or querying a database.
This distinction is the key to understanding concurrency in Python. Let's formalize these concepts.
Speed Up Your Python Program With Concurrency
The article "Speed Up Your Python Program With Concurrency" from Real Python provides a foundational explanation of I/O-bound versus CPU-bound problems. Understanding this difference is essential for choosing the correct tool.
Please read the section titled "When Is Concurrency Useful?" This section clearly defines I/O-bound and CPU-bound processes and includes helpful diagrams.
To summarize:
- CPU-Bound: The program's speed is limited by the CPU. The task involves intensive calculations (e.g., matrix multiplication, image processing, complex simulations). It spends most of its time actively using the CPU.
- I/O-Bound: The program's speed is limited by waiting for slow external resources. The task involves network calls, database queries, or disk access. It spends most of its time waiting, with the CPU being idle.
With this framework, we can now explore Python's three primary concurrency models.
3. Choosing the Right Tool: multiprocessing, threading, and asyncio
Your choice of concurrency model should be driven entirely by the nature of your task.

For CPU-Bound Tasks: Use multiprocessing
If your goal is to parallelize a heavy computational workload across multiple CPU cores, multiprocessing is the only standard library solution in Python.
The multiprocessing module works around the GIL by creating separate operating system processes. Each process has its own Python interpreter and memory space, and therefore, its own GIL. This allows your program to achieve true parallelism.
- How it works: You create a pool of worker processes and distribute tasks among them.
- Trade-offs:
- Pro: Achieves true parallelism for CPU-bound work, fully utilizing all cores.
- Con: Processes are more resource-intensive (higher memory usage) than threads. Communication between processes (Inter-Process Communication or IPC) requires serializing and deserializing data (a process known as "pickling" in Python), which adds overhead.
The Real Python article demonstrates this clearly.
Speed Up Your Python Program With Concurrency
Let's return to the Real Python article to see how these models perform on a CPU-bound problem.
Please read the section "Speeding Up a CPU-Bound Program." Pay close attention to the performance results. Notice how the Multi-Threaded Version and Asynchronous Version are actually slower than the synchronous baseline, while the Process-Based Version provides a significant speedup.
For I/O-Bound Tasks: The threading vs. asyncio Choice
For I/O-bound tasks, both threading and asyncio can provide significant speedups by allowing the program to do useful work while waiting for slow I/O operations to complete.
1. threading
This is the traditional approach to I/O concurrency in Python. When a thread initiates an I/O operation, it releases the GIL, and the operating system can switch to another thread.
- How it works: It uses preemptive multitasking, where the OS decides when to switch between threads.
- When to use it:
- When you need to add concurrency to existing blocking code.
- When you are working with third-party libraries that do not support
asyncio.
- Trade-offs:
- Pro: Conceptually simple for a small number of tasks.
- Con: OS threads have non-trivial memory and creation overhead. Reasoning about shared state requires explicit locks (like the one in our LFU cache), which can lead to complexity, race conditions, and deadlocks. It doesn't scale as well as
asyncioto thousands of concurrent connections.
2. asyncio
This is Python's modern framework for I/O concurrency, built around an event loop and coroutines. This model should feel very familiar given your experience with Rust's Tokio.
- How it works: It uses cooperative multitasking within a single thread. Code explicitly signals when it can be paused using the
awaitkeyword. The event loop then runs another task. This is possible because all I/O operations are non-blocking. - When to use it:
- For new, high-performance I/O-bound applications (e.g., web servers, API clients, database clients).
- When you need to manage a very large number of concurrent connections (tens of thousands or more) efficiently.
- Trade-offs:
- Pro: Extremely efficient with very low overhead per task. Explicit
awaitpoints make it easier to reason about when task switches occur, often reducing the need for locks. - Con: Requires a change in programming style (
async/await). You must use libraries specifically designed forasyncio(e.g.,aiohttpinstead ofrequests). A single long-running, blocking (non-async) function call can freeze the entire event loop.
- Pro: Extremely efficient with very low overhead per task. Explicit
Let's watch a video that demonstrates all three approaches and summarizes the key differences.
AsyncIO VS Threading VS Multiprocessing in Python
The video "AsyncIO VS Threading VS Multiprocessing in Python" from Indently provides a practical comparison and a clear final summary.
Watch the sections on multiprocessing (05:25 - 06:22), asyncio (08:57 - 10:00), and the final summary (12:09 - 12:48). This will reinforce the decision-making framework.
4. A Decision-Making Framework
You can use a simple decision tree to choose the right tool:
-
Is your task CPU-bound?
- Yes: Use
multiprocessingto achieve true parallelism across CPU cores.
- Yes: Use
-
Is your task I/O-bound?
- Yes: Your primary choice should be
asynciofor its efficiency and scalability, especially for new projects. - However, if you are working with a legacy codebase or critical libraries that are blocking and not
async-compatible,threadingis a viable and necessary alternative.
- Yes: Your primary choice should be
Python Concurrency: Threads, Processes, and asyncio Explained
For a final perspective, this blog post offers a concise summary of the advantages and disadvantages of each model, which is helpful for interview-style discussions.
Read the "Summary" section. It provides an excellent breakdown of the pros and cons of asyncio vs. threads for I/O-bound work.
A Quick Note: The Future is No-GIL
It's worth noting that the Python core development team has accepted a proposal (PEP 703) to make the GIL optional. An experimental "no-GIL" build is available in Python 3.13. While it's not yet production-ready and has some performance trade-offs for single-threaded code, it signals a future where threading may be able to achieve true parallelism for CPU-bound tasks in Python. This is a great topic to be aware of for your interviews.
Conclusion
Today, we've dissected one of the most debated features of Python and established a clear mental model for navigating its concurrency landscape.
Key Takeaways:
- The Global Interpreter Lock (GIL) is a mutex in CPython that prevents multiple threads from executing Python bytecode simultaneously, limiting
threadingto concurrency, not parallelism. - The first step is always to identify your problem: is it CPU-bound or I/O-bound?
- For CPU-bound tasks, use
multiprocessingto bypass the GIL and leverage multiple cores. - For I/O-bound tasks, prefer
asynciofor its superior efficiency and scalability. Usethreadingwhen you must integrate with blocking code.
Next Lesson Preview
Now that we have a solid theoretical foundation, we will start applying these concepts. In the next lesson, we will dive deeper into the threading model. Building on our LFU cache exercise, we will implement thread-safe counters and data structures using threading.Lock and threading.RLock, exploring the nuances of creating correct and robust concurrent objects.