Hands-on ProcessPool vs ThreadPool lab: pure-Python threads lose to processes, hashlib threads help, and sleep I/O favors threads.
5 min read
“Just add more threads” is the wrong answer for pure-Python CPU work. The GIL makes that a trap. “Always use processes” is also wrong when the work is waiting, not computing.
This is a hands-on lab with measured numbers:
ThreadPoolExecutor vs ProcessPoolExecutor vs serial on 16 identical tasks.hashlib.sha256 (releases the GIL in C), and time.sleep (I/O-wait).Related links:
Lab honesty (30 Sep 2026 IST): Shared Linux lab box (8 cores, kernel 6.12). Python 3.13.5. concurrent.futures only. Local CPU + sleep — no sockets, no Docker, no GPU, no API keys. Affiliates: 0.
Verdict up front: pure-Python CPU — threads 0.82× at 4 workers (slower than serial); processes 2.41× / 3.48× . hashlib — threads 1.60× ; processes still 3.17×. Sleep I/O — threads 7.87× vs processes 6.30× .
| Arm | Work per task | GIL behavior |
|---|---|---|
| Pure-Python CPU | math.sqrt / sin loop, 350k iters | Holds the GIL |
| hashlib SHA-256 | 40k × 4 KiB updates (~160 MiB hashed) | Releases in C |
| Sleep I/O-wait | time.sleep(0.05) | Released while waiting |
Pool API: ThreadPoolExecutor / ProcessPoolExecutor with max_workers ∈ . Metric: wall time p50 over 3 rounds for all 16 tasks.
Related links:
| Workers | Thread p50 s | Process p50 s | Serial |
|---|---|---|---|
| 1 | 0.518 | 0.556 | 0.510 |
| 2 | 0.607 | 0.342 | — |
| 4 | 0.619 (0.82×) | 0.211 (2.41×) | — |
| 8 | 0.668 | 0.146 (3.48×) | — |
Threads never beat serial here — more workers made it worse. Processes scaled roughly with cores until spawn/overhead flattened the curve.
| Workers | Thread p50 s | Process p50 s | Serial |
|---|---|---|---|
| 1 | 1.799 | 1.755 | 1.819 |
| 2 | 1.250 | 0.941 | — |
| 4 | 1.140 (1.60×) | 0.574 (3.17×) | — |
| 8 | 1.580 (regressed) | 0.509 (3.57×) | — |
C extensions that release the GIL let threads help — but processes still won big, and thread w=8 regressed vs w=4 on this shared box (contention / scheduling noise).
| Workers | Thread p50 s | Process p50 s | Serial |
|---|---|---|---|
| 1 | 0.805 | 0.815 | 0.802 |
| 4 | 0.205 | 0.215 | — |
| 8 | 0.102 (7.87×) | 0.127 (6.30×) | — |
When tasks mostly wait, threads are enough — and slightly cheaper than forking eight workers for 50 ms sleeps.
Related links:
hashlib, many crypto/compress codecs) — measure; do not assume.Reach for processes when the hotspot is pure Python (or any code that holds the GIL for long stretches).
ProcessPoolExecutor (or another process model); do not expect thread speedups.On this 8-core box with 16 tasks: pure-Python CPU — threads lost (0.82× workers); processes 2.41× / 3.48× . hashlib — threads 1.60× , processes 3.17×. Sleep I/O — threads 7.87× edged processes 6.30× at 8 workers. Match the pool to the GIL story, not the folklore.
Evidence path on the lab box: lab-evidence/24-process-vs-thread-pool/results/. Affiliates: 0.
Lab evidence
Lab 30 Sep 2026 IST. Python 3.13.5 concurrent.futures. 16 tasks on 8-core box. Pure-Python CPU: serial 0.510s; thread w4 0.619s (0.82× slower); process w4 0.211s (2.41×); process w8 0.146s (3.48×). hashlib.sha256: serial 1.819s; thread w4 1.140s (1.60×); process w4 0.574s (3.17×); thread w8 regresses to 1.580s. sleep(0.05): serial 0.802s; thread w8 0.102s (7.87×); process w8 0.127s (6.30×). No Docker. Affiliates: 0. Evidence: lab-evidence/24-process-vs-thread-pool/.
Hands-on ThreadPoolExecutor vs sequential lab: real wall-time speedup for I/O sleep workloads plus GIL CPU contrast, measured on Linux localhost (lab).
Lab1 Oct 20264 min read
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.
Lab1 Oct 20264 min read