Understanding the Global Interpreter Lock (GIL) in Python
What is Global Interpreter Lock (GIL) in Python?
If you’ve ever tried to write multithreaded Python code and noticed it doesn’t quite speed things up, especially on multi-core CPUs - you’re not alone. Python has a built-in mechanism that limits how threads are executed, and it’s called the Global Interpreter Lock, or GIL.
In Python, GIL is a mutex (or a lock) that allows only one thread to execute Python bytecode at a time, even on machines with multiple cores. While your system might have 4, 8, or even 16 CPU cores, Python won’t fully utilize them in a multithreaded program. This constraint is specific to CPython, the standard and most widely used Python implementation.
This might sound unusual for developers of languages like Java or C++, where true parallelism with threads is more straightforward. However, in Python, understanding the GIL is key to writing performant concurrent programs.
Let’s try to understand why Python includes a lock that seemingly defeats the purpose of threading.
Fundamentals of Operating Systems
Learn about operating systems by taking a deep dive into each of its main functionalities.Try it for freeWhy does GIL exist in Python?
Python uses a memory management technique called reference counting. In this approach, every object in memory counts how many references point to it. When that count drops to zero (when no references remain), the object is automatically deallocated. This mechanism helps manage memory without requiring a separate garbage collection process in most cases.
Let’s look at how this becomes problematic in a multithreaded environment. For example, in the following code, the global variable counter
is referenced by two threads:
import threadingcounter = 0def increment():global counterfor _ in range(100000):counter += 1t1 = threading.Thread(target=increment)t2 = threading.Thread(target=increment)t1.start()t2.start()t1.join()t2.join()print(counter)
This code intends to increment a global variable from two threads. However, the final result is often less than the expected total. This is because the operation counter += 1
is not atomic. It involves reading the value, modifying it, and writing it back. Interference occurs when two threads perform this sequence simultaneously, leading to incorrect results.
Behind the scenes, each +=
operation also involves updating the integer object’s reference count. Without protection, simultaneous access can corrupt memory or lead to crashes.
The Global Interpreter Lock (GIL) was introduced to avoid such issues. The GIL was a deliberate design choice, not an oversight. It ensures that only one thread executes Python bytecode at a time, preventing concurrent modifications of reference counts and other internal data structures.
By enforcing this lock, the GIL simplifies memory management and guarantees thread safety within the interpreter. While this comes at the cost of parallel execution in multithreaded programs, it significantly reduces the complexity of writing safe and stable Python code, particularly in single-threaded scenarios.
In the next section, let’s explore how this lock operates behind the scenes when multiple threads are active in a Python program.
How does GIL work?
In CPython, GIL is managed by a scheduler-like mechanism that controls when threads can enter and leave the execution cycle. Even when multiple threads are launched in a program, only one can execute Python bytecode at a time.
To visualize this behavior, imagine a bouncer at a club. Regardless of how many people (threads) are in line or how large the dance floor (CPU cores) is, only one person at a time is allowed in. Others wait outside until their turn comes. Here’s a diagram to help understand how GIL works:
In this diagram:
- A thread (e.g., Thread A) requests and acquires the GIL before it can run Python bytecode.
- Once it has the GIL, it executes code until one of two conditions occurs:
- A certain number of bytecode instructions are executed (called the switching interval).
- A blocking IO operation is encountered (e.g., a file or network request).
- The GIL is then released, allowing other threads (e.g., Thread B or C) to acquire it.
- The cycle repeats, ensuring that only one thread runs at a time, even on multi-core processors.
This mechanism helps maintain internal consistency in CPython, but it also explains why Python threads don’t achieve true parallelism on CPU-bound tasks.
A pseudo-code capturing the working of GIL will be:
while True:if thread_has_gil():execute_python_bytecode()if bytecode_tick_count >= switch_interval:release_gil()else:wait_for_gil()
This pseudo-code outlines the logic inside the interpreter:
- If a thread has the GIL, it continues executing.
- Once a switching threshold is reached or IO is triggered, the thread releases the GIL.
- Waiting threads compete to acquire the GIL and resume execution.
With this cooperative locking mechanism, the GIL becomes a key factor in how Python handles concurrency. In the next section, let’s explore how it affects CPU-bound vs IO-bound programs.
Impact of GIL on multithreading
GIL directly impacts how Python handles multithreaded programs. While it ensures thread safety, it also prevents multiple threads from executing Python bytecode in parallel, which can become a significant limitation, particularly in CPU-intensive tasks.
However, the effect of the GIL is not uniform. Its impact depends on the type of task being performed. In general, tasks can be categorized as:
- IO-bound tasks: Involve operations that wait on external systems (e.g., disk, network).
- CPU-bound tasks: Involve continuous computation that consumes CPU cycles.
The sections below highlight how the GIL affects each category.
GIL’s impact on the IO-bound tasks
IO-bound tasks spend much of their time waiting—whether it’s for data from a file, a web server, or a database. During these wait periods, Python temporarily releases the GIL, allowing other threads to run.
This behavior makes threading effective for IO-bound operations, as threads can overlap their wait time and improve overall efficiency.
The following examples demonstrate how IO-bound tasks behave when executed sequentially versus concurrently using threads.
Example 1: Sequential IO execution
import timedef fetch_data():print("Starting IO task")time.sleep(2)print("IO task completed")start = time.time()fetch_data()fetch_data()end = time.time()print(f"Sequential execution time: {end - start:.2f} seconds")
Each task waits for 2 seconds before completing. Since the calls are sequential, the total time is approximately 4 seconds. Here’s the output of this code:
Starting IO taskIO task completedStarting IO taskIO task completedSequential execution time: 4.00 seconds
Example 2: Threaded IO execution
import timeimport threadingdef fetch_data():print("Starting IO task")time.sleep(2)print("IO task completed")start = time.time()t1 = threading.Thread(target=fetch_data)t2 = threading.Thread(target=fetch_data)t1.start()t2.start()t1.join()t2.join()end = time.time()print(f"Threaded execution time: {end - start:.2f} seconds")
Since the GIL is released during sleep()
, both threads can run in parallel while waiting. The total execution time drops to approximately 2 seconds. The output of this code is:
Starting IO taskStarting IO taskIO task completedIO task completedThreaded execution time: 2.00 seconds
In real-world scenarios such as web scraping, file handling, or network operations, Python’s ability to release the GIL during blocking IO makes threading a practical and efficient solution.
GIL’s impact on the CPU-bound tasks
CPU-bound tasks involve continuous, computation-heavy logic, such as mathematical operations or data processing loops. Unlike IO operations, these tasks do not release the GIL, causing threads to execute one after another rather than simultaneously.
This makes threading ineffective for speeding up CPU-bound programs in CPython.
Example: Heavy computation using threads
import threadingimport timedef compute():total = 0for _ in range(10**7):total += 1start = time.time()t1 = threading.Thread(target=compute)t2 = threading.Thread(target=compute)t1.start()t2.start()t1.join()t2.join()end = time.time()print(f"CPU-bound threaded execution time: {end - start:.2f} seconds")
Even though two threads are used, they must take turns due to the GIL. The total time taken is roughly the same or even slightly worse than if the computations were run sequentially. No absolute parallelism is achieved.
CPU-intensive programs do not benefit from threading in CPython, as the GIL prevents multiple threads from executing simultaneously, making true parallelism unachievable without alternative approaches. Let us discuss some of these alternative strategies.
Alternatives to working around the GIL
Although GIL restricts parallel execution in threads, different programming strategies can overcome this limitation. These methods allow Python applications to achieve concurrency and performance without modifying or removing the GIL itself.
“You don’t have to remove the GIL - you can outsmart it.”
Let’s explore the most effective workarounds:
Multiprocessing
The multiprocessing
module allows a Python program to spawn independent processes rather than threads. Each process has its own memory space and its own Python interpreter and therefore, each has its own GIL. This allows Python to achieve true parallelism, especially on multi-core machines.
Use Case:
Best suited for CPU-bound tasks that perform heavy computation, such as numerical simulations, data transformations, or image processing.
Example:
from multiprocessing import Processdef compute():total = 0for _ in range(10**7):total += 1p1 = Process(target=compute)p2 = Process(target=compute)p1.start()p2.start()p1.join()p2.join()
In this example, two independent processes are created using multiprocessing.Process
. Each process runs the compute()
function, which performs a CPU-intensive loop. Unlike threads, these processes can run simultaneously on separate CPU cores. The GIL does not block them since each process has its own interpreter instance.
Async IO
asyncio
is Python’s built-in library for writing asynchronous programs. It uses coroutines and an event loop to manage tasks that perform IO operations. Since async functions release control while waiting, other tasks can run in the meantime all within a single thread.
Use Case:
Highly effective for IO-bound workloads like sending API requests, downloading files, or handling user input/output in real-time.
Example:
import asyncioasync def fetch_data():print("Fetching...")await asyncio.sleep(2)print("Done")async def main():await asyncio.gather(fetch_data(), fetch_data())asyncio.run(main())
This example defines an async
function that simulates a 2-second IO wait. Using asyncio.gather()
, both tasks run concurrently within a single thread. The use of await
allows the event loop to switch between tasks during the wait, enabling efficient execution without GIL interference.
C Extensions
Some Python libraries, especially those written in C or Cython, can manually release the GIL during computation. This allows time-consuming tasks to run parallel with other Python threads since those computations no longer require the interpreter’s lock.
Use Case:
Useful for accelerating data processing, numerical computation, and loops using compiled GIL-independent code.
Real-world examples:
- NumPy: Performs operations in compiled C code, releasing the GIL where possible.
- Cython: Allows writing C-level functions in Python syntax and releasing the GIL with nogil: blocks.
- Pandas: Also releases the GIL during certain internal computations.
Alternative Python interpreters
Certain Python interpreters offer different concurrency models or entirely do away with the GIL:
- PyPy: A faster Python implementation with a Just-in-Time (JIT) compiler. Though it still has a GIL, IO-bound tasks perform more efficiently.
- Jython: This runs Python on the Java Virtual Machine (JVM), using Java threads and no GIL.
- IronPython: Targets the .NET runtime and also removes the need for a GIL.
Use Case:
Recommended for projects that need integration with Java/.NET ecosystems or require improved concurrency in IO-heavy applications.
Each of these strategies offers a way to design Python programs that avoid or bypass the limitations of the GIL. Let’s now look beyond workarounds and explore the possibility of removing the GIL entirely in future versions of Python.
The future of GIL
What if Python didn’t need the GIL at all? That’s the idea behind PEP 703, a proposal to make the GIL optional in CPython by allowing developers to compile a version without it. While this could unlock true multithreading and better CPU-bound performance, GIL hasn’t been removed in Python 3 because it simplifies memory management, ensures thread safety, and strengthens single-threaded performance. Removing it introduces complexities like slower single-threaded execution, compatibility issues with C extensions, and the need for fine-grained locking. PEP 703 aims to address this by offering an opt-in GIL-free build, and though it’s still under discussion, it marks a major step toward evolving Python’s concurrency model.
Conclusion
In this article, we explored Global Interpreter Lock (GIL) in Python - what it is, why it exists, how it works, and how it affects multithreading in CPython. Examples showed that IO-bound tasks benefit from threading, but CPU-bound workloads do not. Alternatives like multiprocessing, async IO, native extensions, and interpreters like PyPy and Jython offer ways to work around these limits. With proposals like PEP 703, Python may be heading toward a future where the GIL no longer constrains concurrency.
To explore Python’s concurrency model and learn how to handle multithreading and multiprocessing effectively, check out Codecademy’s Learn Advanced Python 3 course.
Frequently Asked Questions
1. Does Python 3.12 have GIL?
Yes, Python 3.12, like previous CPython versions, still includes the Global Interpreter Lock (GIL). It remains a core part of how CPython ensures thread safety and manages memory using reference counting.
2. Which Python version has no GIL?
Currently, no official release of CPython is GIL-free. However, there is active development around PEP 703, which proposes making GIL optional. Other interpreters like Jython and IronPython do not use GIL.
3. What is the difference between multithreading and multiprocessing?
- Multithreading uses multiple threads within a single process. In CPython, threads are limited by the GIL and can’t run in true parallel for CPU-bound tasks.
- Multiprocessing uses multiple processes, each with its own Python interpreter and memory space, allowing real parallel execution and bypassing the GIL.
4. What is the limitation of GIL in Python?
The main limitation of the GIL is that it prevents multiple threads from executing Python bytecode simultaneously, even on multi-core processors. This restricts the effectiveness of threading for CPU-bound tasks and can hinder performance in parallel computing.
'The Codecademy Team, composed of experienced educators and tech experts, is dedicated to making tech skills accessible to all. We empower learners worldwide with expert-reviewed content that develops and enhances the technical skills needed to advance and succeed in their careers.'
Meet the full teamRelated articles
- Article
What is Python?
What is Python, and what can it do? - Article
How to write Scripts in Python
A comprehensive guide on scripting in Python including writing, running, and debugging a Python Script - Article
Installing Python 3 and Python Packages
Learn how to install Python packages and download Python 3 with Anaconda and Miniconda on Mac and Windows.
Learn more on Codecademy
- Free course
Fundamentals of Operating Systems
Learn about operating systems by taking a deep dive into each of its main functionalities.Beginner Friendly2 hours - Free course
Learn Advanced Python 3: Concurrency
Learn how to use concurrent programming to implement code more efficiently.Advanced2 hours - Free course
Operating Systems: IO Systems
Learn about IO Hardware and Software and how they interact with your Operating System.Beginner Friendly< 1 hour