Skip to content

bgmbgm94/cpython-threading-gil-demo-notes

Repository files navigation

PyCon Busan 2026 CPython Threading / GIL Demo Notes

Personal notes and reconstructed demo code based on a threading/GIL/free-threading demo presented by Petr Andreev at PyCon Busan 2026.

This is not an official repository or original course material.

Experiment overview

This repository is a small CPython threading/GIL experiment. The goal is to run the same workload under different Python interpreters and compare how much parallelism is visible from wall time, process CPU time, and sampled CPU usage.

The core question is not simply “which Python version is faster?” but “how does the same threaded workload behave when it is pure Python bytecode, C-extension work, or I/O-like waiting?”

Numbered comparison targets

ID Python target Why compare it?
T1 Existing normal CPython, such as Python 3.13 or 3.14, with the GIL enabled Baseline for the Python most people already have installed; run this without uv.
T2 uv-managed CPython 3.15 normal build, with the GIL enabled Newer CPython 3.15 behavior while still using the traditional GIL build.
T3 uv-managed CPython 3.15 free-threaded build Main comparison point for seeing how the same threaded workload behaves without the normal GIL bottleneck.

Numbered workload modes

ID Mode What it does What to expect
M1 python-loop Runs a pure Python CPU-bound loop in each thread. Normal CPython should be limited by the GIL and usually stay near one busy core for the Python bytecode part. A free-threaded build can show more parallel CPU use.
M2 hashlib Hashes a large bytes buffer with hashlib.sha256. hashlib uses C/OpenSSL code and can release the GIL for large buffers, so even normal CPython may show multi-core CPU usage.
M3 devnull Writes bytes to the OS null device. This is an I/O-ish comparison. Threads can be useful when work blocks or waits outside Python bytecode execution.

Prerequisites

For the simple existing-Python baseline

Install Python 3.11 or newer. The local examples focus on Python 3.13/3.14 as recent normal CPython builds with the GIL, but smoke runs also pass on uv-managed Python 3.11 and 3.12.

python --version
# Tiny smoke test: 2 threads and 1,000 loops should finish quickly.
python gil_demo.py --mode python-loop --threads 2 --loops 1000

For the CPython 3.15 normal/free-threaded comparison

Install uv. uv downloads and manages the required CPython 3.15 normal and free-threaded builds, so you do not need to install Python 3.15 manually.

# Windows PowerShell
winget install --id astral-sh.uv -e

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Alternative, if you already have Python/pipx
pipx install uv

After installing uv, run:

uv --version
# Longer comparison run: adjust --threads to your CPU.
python run_demo.py --use-uv --build both --mode python-loop --threads 8 --loops 100000000

For CPU/RSS monitoring without uv

Install psutil in the Python environment that runs run_demo.py, or pass --no-monitor. uv-managed runs do this temporarily with uv run --with psutil.

python -m pip install psutil
python run_demo.py --no-use-uv --build gil --mode python-loop

# Or skip monitoring entirely
python run_demo.py --no-use-uv --build gil --mode python-loop --no-monitor

Run the existing-Python baseline (T1)

gil_demo.py itself uses only the Python standard library. You can run it directly with the Python executable you already have installed. This is the simplest way to collect the T1 baseline.

python gil_demo.py --mode python-loop --threads 16 --loops 2000000
python gil_demo.py --mode hashlib --threads 16 --data-mb 128
python gil_demo.py --mode devnull --threads 16 --data-mb 128

run_demo.py --no-use-uv --build gil is the monitored/orchestrated version of the same idea: it uses the normal GIL-enabled Python found on PATH, rather than installing CPython 3.15 through uv.

python run_demo.py --no-use-uv --build gil --mode python-loop --threads 8 --loops 100000000
python run_demo.py --no-use-uv --build gil --mode hashlib --threads 8 --data-mb 128
python run_demo.py --no-use-uv --build gil --mode devnull --threads 8 --data-mb 128

(--threads 8 is only an example; adjust the thread count depending on your CPU.)

When monitoring is enabled, these runs write files named like monitor-system-gil-python-loop.csv so they are not mistaken for CPython 3.15 results. Because uv is not managing the environment in this mode, install psutil in the current Python environment or pass --no-monitor.

Run uv-managed CPython 3.15 normal vs free-threaded (T2/T3)

Use uv when you want run_demo.py to install and run the matching CPython 3.15 normal and free-threaded builds:

python run_demo.py --use-uv --build both --mode python-loop --threads 8 --loops 100000000
python run_demo.py --use-uv --build both --mode hashlib --threads 8 --data-mb 128
python run_demo.py --use-uv --build both --mode devnull --threads 8 --data-mb 128

Those uv-managed runs write files named like monitor-315-gil-python-loop.csv and monitor-315-free-python-loop.csv.

If you already installed a free-threaded executable such as python3.15t on PATH, you can run that without uv:

python run_demo.py --no-use-uv --build free --mode python-loop

Without uv, --build both means “run the system GIL Python, then run python3.15t if it exists on PATH.”

Monitor CPU/RSS/thread count

monitor.py samples a subprocess with psutil and writes a CSV file:

python monitor.py --csv monitor-python-loop.csv -- python gil_demo.py --mode python-loop --threads 8 --loops 100000000

When using run_demo.py, monitoring is enabled by default, so it creates CSV files unless monitoring is disabled. The default CSV names include whether the run came from uv-managed CPython 3.15 or from the system/PATH Python:

python run_demo.py --use-uv --build both --mode python-loop --threads 8 --loops 100000000

Use --no-monitor to run without psutil and without creating CSV files:

python run_demo.py --use-uv --build both --mode python-loop --threads 8 --loops 100000000 --no-monitor
python run_demo.py --no-use-uv --build gil --mode python-loop --threads 8 --loops 100000000 --no-monitor

psutil is the only non-standard dependency, and only needed for monitoring. uv-managed runs add it temporarily with uv run --with psutil; no-uv runs expect it to already be installed in the Python running run_demo.py.

Actual local outputs

Local test machine

The local outputs below were collected on this machine:

Item Value
CPU Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
Physical cores 8
Logical processors / hardware threads 16
os.cpu_count() 16
Reported current clock 2904 MHz

monitor.py uses psutil process CPU percentages. For a multi-threaded process, this number can exceed 100%: roughly one fully busy logical CPU is 100%, eight busy logical CPUs are around 800%, and this 16-logical-processor machine can theoretically report up to about 1600% for one process.

So the ~800% readings below are expected for runs using around eight CPU-heavy worker threads. They mean the process is using roughly eight logical CPUs at once, not that the machine has only eight total percentage points available.

R0. Smoke runs on current python (T1 baseline)

Smoke runs on this machine with the current python on PATH produced the following output. In this run, python was Python 3.14.3 with the GIL enabled.

$ python gil_demo.py --mode python-loop --threads 2 --loops 1000
mode=python-loop threads=2 loops=1,000 data_mb=128 repeats=1 pid=51832 python=3.14.3 gil_enabled=True
done wall=0.001s process_cpu=0.000s cpu/wall=0.00x
sample_result=5000

$ python gil_demo.py --mode hashlib --threads 2 --data-mb 1
mode=hashlib threads=2 loops=2,000,000 data_mb=1 repeats=1 pid=50332 python=3.14.3 gil_enabled=True
done wall=0.003s process_cpu=0.016s cpu/wall=5.09x
sample_result=f399500cd123fca16fcf5ee50b58b53d

$ python gil_demo.py --mode devnull --threads 2 --data-mb 1
mode=devnull threads=2 loops=2,000,000 data_mb=1 repeats=1 pid=49564 python=3.14.3 gil_enabled=True
done wall=0.001s process_cpu=0.000s cpu/wall=0.00x
sample_result=1048575

R1-R5. Monitored local runs

Result ID Target / mode IDs CSV file Duration Peak CPU What it shows
R1 T2 + M1 monitor-315-normal-direct-python-loop-long.csv ~19.791s 105.3% CPython 3.15 normal GIL build running pure Python bytecode mostly stays near one busy logical CPU.
R2 T3 + M1 monitor-315t-direct-python-loop-long.csv ~3.127s 814.3% CPython 3.15 free-threaded build lets the pure Python loop use about eight logical CPUs on this 8-core/16-thread machine.
R3 T2 + M2 monitor-315-normal-hashlib.csv ~0.546s 773.6% hashlib can already use many CPUs under normal CPython because the C/OpenSSL work can release the GIL.
R4 T3 + M2 monitor-315t-hashlib.csv ~0.547s 812.9% Free-threaded does not radically change this short hashlib run because the C-extension path was already parallel-friendly.
R5 T1-style + M3 monitor-devnull.csv ~0.271s 266.8% I/O-like work has a different shape; CPU percentage is not the only useful signal for this mode.

Raw summary text from the local CSV files:

R1 monitor-315-normal-direct-python-loop-long.csv: duration~19.791s peak_cpu=105.3% peak_threads=12 peak_rss=23.1MiB
R2 monitor-315t-direct-python-loop-long.csv: duration~3.127s peak_cpu=814.3% peak_threads=12 peak_rss=23.9MiB
R3 monitor-315-normal-hashlib.csv: duration~0.546s peak_cpu=773.6% peak_threads=12 peak_rss=152.2MiB
R4 monitor-315t-hashlib.csv: duration~0.547s peak_cpu=812.9% peak_threads=12 peak_rss=152.2MiB
R5 monitor-devnull.csv: duration~0.271s peak_cpu=266.8% peak_threads=12 peak_rss=146.7MiB

How to read the comparison

  • Compare R1 vs R2 to see the clearest GIL/free-threaded difference for the pure Python workload (M1).
  • Compare R3 vs R4 to see why C-extension work (M2) can already look parallel even before free-threading.
  • Use R5 as an I/O-like contrast (M3), not as a direct CPU-bound benchmark.
  • On this i7-10700 machine, a peak around 800% means roughly eight logical CPUs were busy. Since the machine has 16 logical processors, the process-level theoretical ceiling is around 1600%.

These numbers are machine- and parameter-dependent. The important signal is the shape of the result: pure Python loop behavior differs most between normal GIL and free-threaded builds, while C-extension and I/O-like workloads can already show parallel or wait-friendly behavior under normal CPython.

About

PyCon Busan 2026 CPython threading/GIL demo notes

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages