Stop by Stop

Concurrency line

Multiprocessing pools

A head chef cannot cook every long dish alone, so it sets up side kitchens and passes each one a copied order.

Words to know

Stops explain each step. The film plays the whole ride.

Explain it
Route
Side kitchensHead chefAsked for kitchensKitchen setupBuilds new kitchensSlip windowSlips go in hereSide kitchen 1Waiting for a slipSide kitchen 2Waiting for a slipPickup windowDishes come back here
  • Setting up a kitchen
  • A slip passed along
  • Finished dishes
  • Something went wrong
Stop 1 of 8

The head chef asks for side kitchens

The head chef has eight heavy dishes, too slow to cook alone. It asks Kitchen setup to build two side kitchens.

Next stop: Both side kitchens open

All stops

The everyday version

The head chef cannot cook everything alone, so it sets up side kitchens. Kitchens share nothing: orders and dishes travel as copied slips.

  • Head chefis the Parent process

    Plans the dishes, hands out the work and collects the finished dishes.

  • Kitchen setupis the Start method

    Builds each side kitchen, empty and ready for orders.

  • Slip windowis the Task queue

    Order slips wait here until a side kitchen takes one.

  • Side kitchen 1is the Worker 1

    One cook working alone, with its own fridge that nobody else can open.

  • Side kitchen 2is the Worker 2

    Cooks at the same time in another kitchen. Whoever finishes first sends first.

  • Pickup windowis the Result queue

    Finished dishes arrive here, in the order they were done.

  • A live cook through the windowis an order that cannot be copied

    The head chef tries to pass an order that has no name, or a phone line that is still open. Only a written copy fits through.

Interview questions

Say the short answer first. Go deeper only if they ask.

Pool vs ProcessPoolExecutor?
Short answer

Both are process pools. The executor has the Future API and fails loudly when a worker crashes.

If they dig deeper

multiprocessing.Pool has map, imap, apply_async and maxtasksperchild. ProcessPoolExecutor offers submit, map, futures, BrokenProcessPool and works with asyncio’s run_in_executor.

Answer that loses points

“They are unrelated; Pool uses threads.”

Why must arguments and results be picklable?
Short answer

They cross a process boundary as bytes.

If they dig deeper

There is no shared memory. Each task is pickled into a pipe and unpickled in the worker. Lambdas, local functions, open files and locks cannot be pickled by default.

Answer that loses points

“Python just passes a reference.”

Why the if __name__ == "__main__": guard?
Short answer

Spawn and forkserver children re-import the main module.

If they dig deeper

Without it, child start-up re-runs your pool-creation code and raises a bootstrapping RuntimeError. The main module must also be safely importable.

Answer that loses points

“It is just a style convention.”

What does chunksize do?
Short answer

It groups items per task to cut communication overhead.

If they dig deeper

Bigger chunks mean fewer pickles and round trips but worse load balancing when item cost varies. Executor.map defaults to 1, which is slow for tiny items on a process pool.

Answer that loses points

“It is the number of workers.”

Does map preserve order?
Short answer

Yes. Results come back in input order.

If they dig deeper

imap_unordered and as_completed return in completion order. That streams results and avoids head-of-line blocking behind one slow item.

Answer that loses points

“Results come back in whatever order they finish.”

What is the default start method on Linux in 3.14?
Short answer

forkserver.

If they dig deeper

It was fork until 3.13. macOS and Windows use spawn. Forking a multithreaded parent can deadlock on inherited locks. Use get_context("fork") explicitly if you really need it.

Answer that loses points

“fork”, or “spawn on Linux”.

How does Celery’s prefork pool relate?
Short answer

It is the same model: a parent plus child worker processes.

If they dig deeper

Prefork is Celery’s default pool. It runs tasks in child processes, built on billiard, a multiprocessing fork, and worker_max_tasks_per_child recycles them. For I/O-bound tasks there are gevent, eventlet and thread pools.

Answer that loses points

“Celery workers are threads.”

How many workers for CPU-bound work?
Short answer

About the number of cores.

If they dig deeper

More processes than cores just context-switch, and memory per worker multiplies. In containers, os.process_cpu_count() can differ from os.cpu_count() because of affinity.

Answer that loses points

“As many as possible; 100 workers is faster.”

Cheat-sheet

Commands you will actually type.

with Pool(4) as p: print(p.map(f, range(100), chunksize=10))

The canonical pool. Put it under `if __name__ == "__main__":`.

with ProcessPoolExecutor() as ex: futs = [ex.submit(f, i) for i in range(10)]

The `Future` API. Read them with `as_completed(futs)`, in completion order.

p.imap_unordered(f, items, chunksize=20)

Stream results as they finish. Avoids head-of-line blocking.

ctx = mp.get_context("spawn"); ctx.Pool(4)

Choose a start method per pool without changing the global default.

mp.get_start_method()   # forkserver on Linux 3.14

What am I running? `spawn` on macOS and Windows.

Pool(4, maxtasksperchild=100)

Recycle workers to cap memory growth. Celery calls it `worker_max_tasks_per_child`.

p.map(partial(f, k=3), items)   # not a lambda

The pickle-safe way to bind extra arguments.

celery -A app worker --pool=prefork --concurrency=4

Celery’s prefork pool: 4 child processes.

Sources