Filter concepts by levelShowing all levels.

Python · Multiprocessing, Futures, and the GIL

Real parallelism, executors, and the GIL

Separate processes for genuine CPU parallelism, one executor API for both threads and processes, and the lock that decides which one actually helps

  • Process, not Threadown interpreter, own memory, own GIL — real parallelism
  • Everything is pickledarguments, results, Queue/Pipe items — all serialized across
  • as_completed()process results in FINISH order, not submission order
  • The GILblocks CPU-bound threading; I/O-bound threading still works

1.What the GIL is, and why it exists

The GIL is a mutex inside CPython letting only one thread execute Python bytecode at a time — even on multiple cores, two threads never run Python code simultaneously. It exists because reference counting is not thread-safe without it.

Key features

  • The GIL is one mutex per CPython process — only one thread runs Python bytecode at a time
  • It exists to make reference counting safe without a lock on every single object
  • Threads take turns — the interpreter switches which one holds the GIL, roughly every 5ms
  • A CPython implementation detail, not a language requirement — Jython/IronPython have none
  • Python 3.13+ has an experimental free-threaded build (PEP 703) — not this machine's build

Where the GIL is, and is not, a CPython implementation detail

Where the GIL is, and is not, a CPython implementation detail
ClaimTrue?
The GIL is part of the Python language specificationfalse — it is specific to CPython, the reference implementation
Every CPython object's reference count needs GIL protection to update safelytrue — this is the reason the GIL exists
Threads never make progress on Python bytecode without holding the GILtrue — by definition, since it gates bytecode execution
The GIL prevents all forms of concurrency in Pythonfalse — I/O-bound concurrency still works well; see the paired concept
One GIL per process — only its holder runs Python bytecode

4 threads

in one process

the GIL

held by exactly one thread at a time

1 thread runs bytecode

the other 3 wait their turn

  1. 4 threads — in one process
  2. the GIL — held by exactly one thread at a time
  3. 1 thread runs bytecode — the other 3 wait their turn

Command

python
import sys

# True on a standard build; False on a free-threaded (PEP 703) 3.13+ build
print(sys._is_gil_enabled())

The GIL is one mutex per CPython process, letting only one thread run bytecode at a time. A CPython detail, not a language rule.

2.The GIL in practice — CPU-bound vs. I/O-bound, and what to reach for

For CPU-bound work, threads do not speed things up — the GIL lets only one run bytecode at a time. For I/O-bound work, threads genuinely help, since waiting releases the GIL. Multiprocessing for CPU parallelism; threading/async for waiting.

Key features

  • CPU-bound + threading: little/no speedup — measured here, 2 threads was SLOWER than serial
  • CPU-bound + multiprocessing: real speedup — each process has its own GIL, runs in parallel
  • I/O-bound + threading: real speedup — a blocked thread releases the GIL for others to use
  • A C extension (NumPy, many I/O libs) can release the GIL internally during native code
  • Rule of thumb: CPU-bound → multiprocessing; I/O-bound → threading or asyncio

Measured effect of the GIL, this machine, Python 3.14.3

Measured effect of the GIL, this machine, Python 3.14.3
WorkloadSerialThreaded (2)Multiprocess (2)
CPU-bound (5M-iteration sum, twice)0.641s0.757s — slower0.470s — faster
I/O-bound (0.3s sleep, twice)0.601s0.302s — ~2x fasternot applicable
Match the tool to the workload

CPU-bound?

busy computing, not waiting

multiprocessing

separate GILs — real parallelism

I/O-bound?

mostly waiting on network/disk

  1. CPU-bound? — busy computing, not waiting
  2. multiprocessing — separate GILs — real parallelism
  3. I/O-bound? — mostly waiting on network/disk

Command

python
# Decision: what is the code actually doing while it "works"?
#   Busy computing (loops, math, parsing)     -> CPU-bound  -> multiprocessing
#   Waiting (network, disk, sleep, DB query)   -> I/O-bound   -> threading / asyncio

CPU-bound: reach for multiprocessing — threading rarely helps, can be slower. I/O-bound: reach for threading/asyncio — a blocked thread releases the GIL.

Remember

  • A Process target function must be defined at module level — a nested function has no importable name pickle can send to the child.
  • ex.submit(fn, *args) returns a Future immediately; future.result() blocks and re-raises the task's real exception.
  • future.cancel() only succeeds on a task that has not started running yet — a running task must cooperate via its own stop signal.
  • CPU-bound + threading measured SLOWER than serial here (GIL contention overhead) — multiprocessing measured genuinely faster.

Pro tip

  • On Windows, the default start method is "spawn" — always guard process-creating code with if __name__ == "__main__":.
  • ThreadPoolExecutor for I/O-bound work, ProcessPoolExecutor for CPU-bound work — same submit()/map() API either way.
  • A C extension can release the GIL during its native code — this is why NumPy math can scale across threads despite the GIL.
  • Decide the tool from what the code is actually doing while it "works" — computing points at multiprocessing, waiting points at threading/async.

Next up

  • Async Python

Sources

  • multiprocessing — docs.python.org/3/library/multiprocessing.html
  • concurrent.futures — docs.python.org/3/library/concurrent.futures.html
  • Global Interpreter Lock — docs.python.org/3/glossary.html#term-global-interpreter-lock
  • Every example executed on Python 3.14.3, standard (GIL-enabled) build · checked 2026-08-21

Spotted a mistake on this page? Tell us.