Skip to content
← All Python articles

Python explainer

Speed Up CPU-Heavy Work with a Process Pool

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

Time the same pure-Python work as a plain loop, a thread pool and a process pool, and see why only the process pool uses several cores.

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

In the concurrency track, a thread pool made waiting faster but did nothing for code that computes. A process pool fixes that. Save this script as speedup.py and run it with python speedup.py:

import time
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor


def count_primes(limit: int) -> int:
    '''Count the primes below limit, the slow way, in pure Python.'''
    count = 0
    for n in range(2, limit):
        if all(n % d for d in range(2, int(n**0.5) + 1)):
            count += 1
    return count


def main() -> None:
    jobs = [500_000] * 4

    start = time.perf_counter()
    plain = [count_primes(n) for n in jobs]
    print(f"plain loop:   {time.perf_counter() - start:.1f}s")

    start = time.perf_counter()
    with ThreadPoolExecutor(max_workers=4) as pool:
        threaded = list(pool.map(count_primes, jobs))
    print(f"thread pool:  {time.perf_counter() - start:.1f}s")

    start = time.perf_counter()
    with ProcessPoolExecutor(max_workers=4) as pool:
        processed = list(pool.map(count_primes, jobs))
    print(f"process pool: {time.perf_counter() - start:.1f}s")

    assert plain == threaded == processed


if __name__ == "__main__":
    main()

On a 10-core laptop with Python 3.15, it printed:

plain loop:   1.8s
thread pool:  1.8s
process pool: 0.6s

Your numbers will differ, but the pattern holds on any machine with several cores. The thread pool is no faster than the loop. The process pool is about three times faster with four workers.

Why processes help

Each worker in a process pool is a separate Python process, with its own interpreter and its own GIL. Four processes can run Python code on four cores at the same time. Four threads in one process take turns on one GIL.

ProcessPoolExecutor has the same methods as ThreadPoolExecutor. map keeps the input order, submit returns a Future, and the with block waits for every job.

What a process pool needs from your code

  • The function must be importable. Pickle sends a function as its module and name, not as its code, and the worker looks it up by that name. Define the function at the top level of a module, not inside another function.
  • The pool must start under if __name__ == "__main__":. Depending on how Python starts workers, a worker can run your module's top-level code again. Without the guard, each worker would try to start its own pool.
  • Arguments and results must be picklable. Pickling is how Python turns an object into bytes to send it to another process. Numbers, strings, lists, dicts and top-level functions pickle. Lambdas, nested functions, open files and locks do not.

What a process pool costs

Starting a process takes far longer than starting a thread, and Python copies every argument and result between processes. A process pool is faster only when each job does a lot of computing compared with the data it sends. Thousands of tiny jobs can run slower than a plain loop, and a later article shows how to batch them.

Why the exercises here simulate

The runtime that grades your exercises runs your code in a way that cannot send your own functions to a worker process. The exercises in this track therefore check, plan or simulate what a process pool does, and some run provided code. Run this article's script on your own machine to see a real pool at work.

Check your understanding You move a CPU-heavy function into a process pool, and it gets slower. Each call takes about a millisecond and receives a large list. What is the likely cause? Show answer

The cost of sending the data is bigger than the work. Each call pickles the list, copies it to a worker, and unpickles it there, and that can take longer than the millisecond of computing. Send less data per call, or give each call more work, for example by handling many items per job.

Going further

Python's free-threaded build can run threads on several cores without a process pool. A later article in this track covers it.

step 1 of 24

Parallel Python with Process Pools

Up next Will It Pickle? Check What a Process Pool Can Send