All posts
Knowledge Backend Concurrency May 12, 2026

Python GIL: Why Multiple Threads Do Not Mean Parallel Python

The GIL means one CPython process can run only one Python-bytecode thread at a time. This is a threading issue, not an async/await issue.

The GIL is one of the most misunderstood parts of Python backend concurrency.

The short version:

Inside one CPython process,
multiple threads can exist,
but only one thread can execute Python bytecode at a time.

That is a thread-level constraint. It is not the reason async code runs one coroutine at a time.

中文记忆:

GIL 管的是“多线程谁能执行 Python bytecode”。
async/await 管的是“协程什么时候让出 event loop”。
它们不是同一层问题。

Python GIL: Threads vs Async

What the GIL Is

GIL means Global Interpreter Lock.

In CPython, the most common Python implementation, the GIL is an interpreter-level lock. Before a thread can execute Python bytecode, it must hold the GIL.

This means:

One process
├── Thread A wants to run Python bytecode
├── Thread B wants to run Python bytecode
└── GIL allows only one of them to run Python bytecode at a time

The key phrase is:

execute Python bytecode

Threads can still exist. They can still be scheduled by the operating system. They can still wait on I/O. But if they are doing CPU-heavy Python work, they compete for one interpreter lock.

The Most Important Rule

Inside one CPython process:

many threads can be alive
many threads can be waiting
but only one thread can be running Python bytecode

中文记忆:

多线程不是不能创建。
是同一个 CPython 进程里,同一时刻只有一个线程能真正跑 Python bytecode。

That is why CPU-heavy Python code does not become faster just because you add more threads.

Example:

def cpu_work():
    total = 0
    for i in range(50_000_000):
        total += i
    return total

Running this in two Python threads does not make two CPU cores execute Python bytecode in parallel inside the same process. The threads take turns holding the GIL.

What the GIL Does Not Mean

The GIL does not mean:

Python cannot do concurrency.
Python cannot have multiple threads.
Python async is caused by the GIL.
One Python process cannot handle multiple I/O requests.

Those are different claims.

The GIL specifically limits parallel execution of Python bytecode across threads in one CPython process.

Why I/O Threads Still Work

Threads are still useful for I/O-bound work.

For example:

call database
call external API
read file
wait for network

When a thread waits on I/O, it usually does not need to hold the GIL. The interpreter can let another thread run Python bytecode while the first thread is waiting.

That is why this distinction matters:

Workload Do threads help? Why
I/O-bound Often yes Waiting threads can release the GIL
CPU-bound Python Usually no Threads compete for Python bytecode execution

中文记忆:

I/O-bound: 线程大部分时间在等,GIL 影响小。
CPU-bound: 线程都想跑 Python,GIL 影响大。

Why Async Has Nothing to Do with Bypassing the GIL

async and await are about coroutine scheduling.

An async program usually has one event loop running on one thread:

one process
└── one event-loop thread
    ├── coroutine A
    ├── coroutine B
    └── coroutine C

Only one coroutine runs at a time on that event-loop thread. A coroutine gives up control when it reaches an await.

That behavior is not caused by the GIL. It is caused by the event loop model.

GIL:
  controls which OS thread can execute Python bytecode

async/await:
  controls when a coroutine yields back to the event loop

So this statement is wrong:

Async runs one task at a time because of the GIL.

Better statement:

Async runs one coroutine at a time because the event loop is single-threaded.
The GIL is about Python threads, not coroutine scheduling.

Thread Switching vs Coroutine Switching

Threads are scheduled by the operating system.

Thread switching = preemptive
The OS can interrupt a thread and run another one.

Coroutines are scheduled by the event loop.

Coroutine switching = cooperative
A coroutine yields only at await.

This is why async code is easier to reason about in some I/O-heavy paths:

async def handler():
    step_1()
    step_2()
    await call_database()
    step_3()

Between step_1() and await call_database(), another coroutine will not run unless this code explicitly awaits.

The GIL is not the mechanism doing that. The event loop is.

Multiple Processes Bypass the GIL

The GIL is per process.

process 1 -> its own GIL
process 2 -> its own GIL
process 3 -> its own GIL

That is why CPU-bound Python work usually needs multiple processes, not multiple threads.

Examples:

multiprocessing
process pool
Uvicorn workers
Celery workers
separate service workers

For FastAPI/Uvicorn:

uvicorn app.main:app --workers 4

This starts multiple worker processes. Each worker has its own interpreter and its own GIL. That can use multiple CPU cores for Python bytecode execution.

But remember:

workers are processes, not threads

C Extensions Can Release the GIL

Some native extensions can release the GIL while doing heavy work outside the Python interpreter.

Examples often include numerical or data libraries when operating in native code:

NumPy
pandas
some compression libraries
some crypto libraries

But this depends on the implementation. Do not assume all C extensions release the GIL.

The safe mental model:

Pure Python CPU-heavy code: GIL serializes threads.
Native code may release the GIL, but verify the library.

Common Interview Traps

Wrong statement Better answer
Python cannot have multiple threads Python can have multiple threads; the GIL limits parallel Python bytecode execution
The GIL means only one request can be handled I/O-bound servers can handle many concurrent requests
Async bypasses the GIL Async is coroutine scheduling, not a GIL bypass
The GIL is why async tasks switch Async tasks switch at await because of the event loop
Threads always improve performance Threads help I/O-bound work, not CPU-heavy Python bytecode
More threads fix CPU-bound Python Use processes or native code that releases the GIL

Decision Table

Situation Better tool
Many network/database waits Async or threads
Blocking library in a web route Thread pool
Many WebSocket connections Async
Pure Python CPU-heavy computation Multiple processes
CPU-heavy native library that releases GIL Threads may help, verify
Need parallelism across cores for Python code Processes

How to Explain It in an Interview

Use this:

The GIL is a CPython interpreter lock. In one process, only one thread can execute Python bytecode at a time. That means Python threads do not give true parallelism for CPU-bound Python code. But threads can still help I/O-bound work because waiting on I/O usually releases the GIL. Async is a different concept: it is cooperative coroutine scheduling on an event loop, and coroutine switching happens at await, not because of the GIL. For CPU-bound Python work, use multiple processes or native code that releases the GIL.

Short version:

GIL limits Python bytecode across threads.
async controls coroutine scheduling.
For CPU-bound Python, use processes.
For I/O-bound work, threads or async can both help.

中文版本:

GIL 解决的是 CPython 解释器内部线程执行 bytecode 的互斥问题。
async/await 解决的是协程在 I/O 等待时如何让出 event loop。
CPU-bound Python 多线程会被 GIL 串行化。
I/O-bound 多线程影响小,因为等待 I/O 时通常会释放 GIL。

Thanks for reading.

© 2026 Alan Wang