Skip to content
← Back to explainers

Python explainer

Threads, Processes and the GIL

About 4 min to read. Allow extra time to work through examples.

See why threads speed up waiting but not computing in Python, when to use processes instead, and why the GIL does not make shared data safe.

This article uses Python. You can make it your practice mode to browse related content across the site.

Start four threads that each wait one second for a web server. They finish in about one second, not four. Now start four threads that each add up the numbers below 10 million. On a standard Python install, they take about as long as doing the four sums one after another.

The difference comes from the GIL.

One thread runs Python at a time

The global interpreter lock, or GIL, is a lock inside the standard Python interpreter, CPython. A thread must hold the GIL to run Python code, and only one thread can hold it at a time. So your threads take turns running Python.

A thread releases the GIL while it waits. Reading from a socket, waiting for a database, time.sleep() and most file operations all release it. While one thread waits, another can run.

import time
from concurrent.futures import ThreadPoolExecutor


def wait_for_server(n: int) -> int:
    time.sleep(1)  # stands in for a network call
    return n


def add_up(n: int) -> int:
    return sum(range(n))


start = time.perf_counter()
with ThreadPoolExecutor(max_workers=4) as pool:
    list(pool.map(wait_for_server, range(4)))
print(f"4 waits: {time.perf_counter() - start:.1f}s")  # about 1.0s

start = time.perf_counter()
with ThreadPoolExecutor(max_workers=4) as pool:
    list(pool.map(add_up, [10_000_000] * 4))
print(f"4 sums: {time.perf_counter() - start:.1f}s")  # no faster than one at a time

Threads help when your program is I/O-bound, which means it is slow because it waits for the network, a disk or a database. They do not help when it is CPU-bound, which means it is slow because Python is busy computing.

Use processes for CPU-bound work

A process is a separate running program with its own memory and its own GIL. Several processes can run Python on several CPU cores at the same instant. ProcessPoolExecutor has the same methods as ThreadPoolExecutor:

from concurrent.futures import ProcessPoolExecutor

if __name__ == "__main__":
    with ProcessPoolExecutor() as pool:
        totals = list(pool.map(add_up, [10_000_000] * 4))

Processes cost more than threads, in three ways:

  • Starting a process takes far longer than starting a thread, so give each one a decent amount of work.
  • Processes do not share memory. Python copies arguments and results between them by pickling. Both must be picklable, and large data is slow to send.
  • The child process has to import the function it runs. Define the function at the top level of a module, and start the pool under if __name__ == "__main__":.

Some libraries do their heavy work in C and release the GIL while they do it. NumPy does this for many array operations. With such a library, threads can keep several cores busy. Check the library's documentation, and measure.

The GIL does not make your code thread-safe

The GIL stops two threads running Python at the same instant. It does not stop the interpreter switching to another thread between two steps of your code. Suppose a thread reads a shared value, gets paused, and another thread updates the value in the meantime. When the first thread writes its result back, the other thread's update is lost.

The next article shows a lost update, and how a lock prevents it.

Check your understanding Two web handlers spend most of their time waiting for a database. Should you move them to processes because of the GIL? Show answer

No. Waiting releases the GIL, so threads already overlap the waits. Processes would add startup and pickling costs without making the waits shorter. If the handlers are slow, look at the database queries and the connection pool first.

A quick test is to compare CPU time with wall-clock time for a request. If the CPU time is much smaller, the code is mostly waiting.

Going further

Python 3.13 added an optional free-threaded build that has no GIL, and Python 3.14 made it officially supported. In that build, threads can run Python code on several cores at once. It is not the default install, and some C extensions do not support it yet. Shared data still needs a lock there, as it does with the GIL.

Read more in the free-threading guide and the concurrent.futures documentation.

step 5 of 56

Concurrency with Threads and asyncio

Up next Race Conditions and Locks