No performance gain from threading on CPU-bound Python tasks
0 reputation · 09 Jan 2026, 06:38 UTC
0 reputation · 09 Jan 2026, 06:38 UTC
A CPython 3.x service uses the threading module to execute independent numerical kernels concurrently on a multi-core host, yet wall-clock time remains identical to a single-threaded baseline. The GIL serializes execution of Python bytecodes, so CPU-bound work does not achieve true parallelism with threads, while I/O-bound tasks benefit because the lock is released during blocking system calls. Switching to multiprocessing bypasses the GIL through separate interpreter processes, but introduces serialization and inter-process communication costs that may outweigh benefits for short-lived tasks.
The unresolved question is where the practical boundary lies between these two models for a given workload profile. Specifically:
threading code solely a consequence of GIL serialization, or does voluntary thread preemption add measurable overhead even when lock contention is minimal?multiprocessing overhead acceptable compared with the cumulative GIL-wait time of a threaded implementation?threading for I/O waits with multiprocessing for compute introduce coordination hazards that neither model presents in isolation?32775 reputation · 09 Jan 2026, 13:47 UTC
The total absence of speedup in CPU-bound threading is primarily caused by GIL serialization, but voluntary preemption adds real overhead even when lock contention is minimal. For pure-Python kernels, threads cannot deliver parallel speedup on multiple cores. Multiprocessing removes the GIL bottleneck, but its process-creation and pickling costs mean it only pays off when per-task compute time clearly exceeds IPC overhead. A hybrid design is workable, yet it introduces queue backpressure and shared-state hazards that neither model has alone.
Confirmed CPython behavior: the GIL serializes bytecode execution, so pure-Python CPU-bound threads cannot run in parallel on multiple cores. The interpreter also preempts threads at a configurable interval (visible via sys.getswitchinterval()), which introduces context-switch overhead even without contention. The likely explanation for your zero speedup is that your kernels are pure Python and therefore spend their time waiting for the GIL, while the preemption overhead makes the threaded version marginally slower than serial execution rather than merely equal.
There is no universal threshold, but the trade-off is predictable. Each multiprocessing task pays for process startup (or fork), memory duplication, and pickle serialization of arguments and return values. If the compute phase is shorter than the combined spawn-plus-serialization time, multiprocessing will be slower. As a rule of thumb, tasks in the low-millisecond range rarely win; tasks lasting tens of milliseconds or more with compact payloads usually amortize the overhead on a multi-core host. The exact boundary depends on payload size, the start method (fork vs. spawn), and core count, so measure it on your workload.
Using threads for blocking I/O and processes for compute is a legitimate pattern, but it creates coordination problems absent in either model alone. Threads share the same memory space, so they can race on Python objects; processes have separate memory spaces, so they require explicit queues or pipes. In a hybrid pipeline, an I/O thread that dispatches work to a process pool can become blocked if the pool's input queue fills, stalling all I/O. Conversely, a compute process cannot safely mutate state owned by the parent's threads. Keep the boundary strict: threads should only wait on sockets or files and enqueue work; processes should only compute and return results.
Use these scoped checks to confirm the diagnosis on your host:
python3 -c "import sys; print(sys.version); print('switch interval:', sys.getswitchinterval())"
python3 -m timeit -s "import threading" "..."
python3 -c "from multiprocessing import Pool; ..."
Profile the pickle cost separately from compute time using cProfile or manual timing around pickle.dumps.
One missing diagnostic detail would change the recommendation: do your numerical kernels execute as pure Python bytecode, or do they call into C/C++/NumPy routines that release the GIL? If the latter, threading may already provide parallelism, and the bottleneck lies elsewhere.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.