Skip to the solution
MediumBackend11 min read

When should you still use multiprocessing vs free-threaded threads in Python 3.12+?

272 views

The problem

Question presented to candidate: "Now that CPython has a free-threaded build, threads can finally run Python code on several cores. Is there still a reason to reach for multiprocessing? How would you decide between processes and free-threaded threads for a CPU-heavy job?"

What a strong answer should cover:

  • Version reality first: there is no free-threaded build in 3.12; it is experimental in 3.13 and supported but optional in 3.14. On the default build, CPU-bound pure-Python work still needs processes.
  • What threads win on: no pickling, cheap start-up, shared objects by reference, low memory β€” best when tasks share large in-memory state or exchange data often.
  • What processes still win on: they work on every build and with every C extension (one extension without free-threading support re-enables the GIL), they give memory isolation (no data races by default) and crash isolation (a segfault kills one worker, not the app), and the OS can kill or limit them.
  • The costs of processes: pickling every argument and result, start-up time (especially spawn), duplicated memory; mitigate with chunking, initializer, and shared_memory.
  • Measure: single-threaded slowdown of the free-threaded build, the size of data crossing the boundary, and real wall-clock time on the target machine.

Clarifying questions expected:

  • Which Python version and build will production run, and do all compiled dependencies support free threading?
  • How much state do the workers share, and how often do they exchange data?

Code / implementation expected: Optional β€” the same function run on a ThreadPoolExecutor and a ProcessPoolExecutor, with the trade-offs explained.

  • gil
  • threading
  • performance

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

11 min read Β· 12 sections

Run the code

Edit the code and run it here. Each console line with an expected value becomes a test.

PythonThe process tax: bytes pickled per design, plus a decision helper
PythonMemory and crash isolation: what only processes give you
# Two reasons processes still win even when threads can run in parallel:
# memory isolation and crash isolation. Runs on CPython (not the browser).
import multiprocessing as mp
import os
import sys
import sysconfig
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from concurrent.futures.process import BrokenProcessPool

CACHE = {"hits": 0}


def touch_cache(_):
    CACHE["hits"] += 1          # threads: one shared CACHE; processes: one each
    return CACHE["hits"]


def crunch(n):
    total = 0
    for i in range(n):
        total += i * i % 7
    return total


def crash(n):
    if n == 2:
        os._exit(1)             # simulates a segfault in a C extension
    return n


if __name__ == "__main__":
    print("free-threaded build:", bool(sysconfig.get_config_var("Py_GIL_DISABLED")))
    print("GIL enabled:", sys._is_gil_enabled())
    ctx = mp.get_context("spawn")

    # Same function, same answers, either executor
    with ThreadPoolExecutor(4) as tp, ProcessPoolExecutor(4, mp_context=ctx) as pp:
        t = list(tp.map(crunch, [100_000] * 4))
        p = list(pp.map(crunch, [100_000] * 4))
    print("threads == processes:", t == p)

    # Memory isolation: threads share module state, processes do not
    with ThreadPoolExecutor(2) as tp:
        list(tp.map(touch_cache, range(6)))
    print("parent CACHE after threads:", CACHE["hits"])
    with ProcessPoolExecutor(2, mp_context=ctx) as pp:
        list(pp.map(touch_cache, range(6)))
    print("parent CACHE after processes:", CACHE["hits"])

    # Crash isolation: a dying worker breaks the pool, not the parent
    try:
        with ProcessPoolExecutor(2, mp_context=ctx) as pp:
            print(list(pp.map(crash, range(4))))
    except BrokenProcessPool as exc:
        print("parent survived:", type(exc).__name__)
    print("parent still running, pid is an int:", isinstance(os.getpid(), int))

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 102 of 139 in Python

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Python questions
Up nextMedium

What is monkey patching in Python?

Continue