The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
List available start methodsmethods = multiprocessing.get_all_start_methods()View examples
Create an explicit contextctx = multiprocessing.get_context('spawn')View examples
Set the global start methodmultiprocessing.set_start_method('spawn')View examples
Guard application startupif __name__ == '__main__': main()View examples
Start one processprocess = ctx.Process(target=worker, args=(input_value,)); process.start()View examples
Wait for process completionprocess.join(timeout=5)View examples
Release a Process handleprocess.close()View examples
Map work through a poolwith ctx.Pool(processes=4) as pool: results = pool.map(transform, items, chunksize=32)View examples
Submit an asynchronous pool callpending = pool.apply_async(transform, (item,)); result = pending.get(timeout=5)View examples
Drain and close a poolpool.close(); pool.join()View examples
Send a queued messagequeue.put(message); received = queue.get(timeout=5)View examples
Create a one-way pipereceiver, sender = ctx.Pipe(duplex=False)View examples
Wait for ready connectionsready = multiprocessing.connection.wait(connections, timeout=5)View examples
Signal workers with an Eventready.set(); ready.wait(timeout=5)View examples
Protect a critical sectionwith lock: shared_value.value += 1View examples
Create a synchronized scalarcounter = ctx.Value('i', 0)View examples
Create a shared-memory blockblock = SharedMemory(create=True, size=byte_count)View examples
Attach by shared-memory nameblock = SharedMemory(name=block_name)View examples
Close and unlink shared memoryblock.close(); block.unlink()View examples
Opt out of resource trackingblock = SharedMemory(name=block_name, track=False)View examples
Run a manager serverwith ctx.Manager() as manager: shared = manager.dict()View examples
Create a managed listshared_items = manager.list(initial_items)View examples
Receive only trusted objectsmessage = connection.recv()View examples
Use an importable workerdef worker(payload): return process_payload(payload)View examples
Request cooperative shutdownstop_event.set(); process.join(timeout=5)View examples
Terminate an unresponsive processif process.is_alive(): process.terminate(); process.join()View examples

multiprocessing runs Python work in separate processes, enabling CPU parallelism on ordinary CPython builds at the cost of process startup, serialization, and explicit resource ownership. Keep worker functions importable, protect application startup with a main guard, choose a start context deliberately when consistency matters, and prefer messages over shared mutable state. When copying large buffers dominates the workload, shared memory can help—but only with strict synchronization and a single, documented cleanup owner.

Step by step

Detailed examples

01

Choose a start context as part of the application contract

The start method controls what a child inherits and how it imports application code. In Python 3.14, forkserver became the POSIX default where supported; spawn remains the default on macOS and Windows, and fork is no longer the default on any platform. Spawn starts a fresh interpreter, while forkserver asks a usually single-threaded server to fork children; both require picklable arguments and an importable main module. Fork can be fast but is unsafe around many multithreaded runtimes, and os.fork() may emit a DeprecationWarning when Python detects multiple threads. Libraries should accept a caller-provided context instead of globally selecting one. Applications that need consistency may use get_context(), while set_start_method() belongs inside the main guard and normally runs only once.

Select a portable context without global mutation
import multiprocessing as mp

def main() -> None:
    context = mp.get_context('spawn')
    print(context.get_start_method())
    print('spawn' in mp.get_all_start_methods())

if __name__ == '__main__':
    main()
Output
spawn
True
Back to quick reference ↑
02

Guard startup and own every Process lifecycle

With spawn and forkserver, the child imports the main module, so top-level process creation can recurse indefinitely. Put startup in a function, call it under if __name__ == '__main__', and keep targets at module scope. start() launches once; join() waits but does not stop a child, and a timeout merely returns. After joining, check exitcode rather than assuming success. A normal return is zero, an uncaught exception is usually one, and POSIX signal exits are negative. close() releases the parent-side Process resources only after the child has stopped. Explicit ownership avoids zombie processes on POSIX and leaked handles elsewhere.

Collect a result before joining its producer
import multiprocessing as mp

def double(value: int, output: mp.Queue) -> None:
    output.put(value * 2)

def main() -> None:
    context = mp.get_context('spawn')
    output = context.Queue()
    process = context.Process(target=double, args=(21, output))
    process.start()
    print(output.get(timeout=5))
    process.join(timeout=5)
    print(process.exitcode)
    output.close()
    process.close()

if __name__ == '__main__':
    main()
Output
42
0
Back to quick reference ↑
03

Reuse a bounded Pool for independent CPU tasks

A Pool amortizes worker startup across many independent calls. map() preserves input order; imap_unordered() can reduce head-of-line blocking when order is irrelevant. Tune chunksize against real item cost because larger chunks reduce IPC overhead but can hurt load balancing. AsyncResult.get() transports worker exceptions back to the parent and can bound the wait. Consume every result so failures are visible. close() followed by join() is graceful; terminate() abandons outstanding work. A Pool context manager calls terminate() on exit, so ensure required results are consumed inside the block and use an explicit close/join lifecycle when accepted tasks must drain during surrounding error handling.

Map top-level work with stable result ordering
import multiprocessing as mp

def square(value: int) -> int:
    return value * value

def main() -> None:
    context = mp.get_context('spawn')
    with context.Pool(processes=2) as pool:
        results = pool.map(square, [4, 2, 3], chunksize=1)
    print(results)

if __name__ == '__main__':
    main()
Output
[16, 4, 9]
Back to quick reference ↑
04

Prefer explicit messages and drain them before joining producers

Queue supports multiple producers and consumers and serializes each object through a pipe using a feeder thread. A producer that has queued buffered data may not exit until that data is flushed, so receive expected messages before joining it; joining first can deadlock when the pipe fills. close() and join_thread() let the owning process finish its feeder deliberately. Pipe() is lighter for point-to-point traffic: with duplex=False, the first endpoint receives and the second sends. Do not let multiple threads or processes concurrently use the same pipe endpoint because messages may be corrupted. Both Queue and Connection receive operations unpickle objects, so their inputs must be trusted.

Use distinct ends of a one-way pipe
import multiprocessing as mp

def produce(sender) -> None:
    sender.send({'status': 'ready', 'count': 3})
    sender.close()

def main() -> None:
    context = mp.get_context('spawn')
    receiver, sender = context.Pipe(duplex=False)
    process = context.Process(target=produce, args=(sender,))
    process.start()
    sender.close()
    message = receiver.recv()
    receiver.close()
    process.join(timeout=5)
    print(message['status'])
    print(message['count'])

if __name__ == '__main__':
    main()
Output
ready
3
Back to quick reference ↑
05

Share the smallest state and synchronize the whole invariant

Event communicates a state change without busy-waiting; wait(timeout) lets a worker retain a shutdown or recovery path. Lock protects a critical section across processes and should be used as a context manager. Value and Array place ctypes values in shared memory and are synchronized by default, but an expression such as value.value += 1 is a read-modify-write sequence and is not automatically atomic as a whole. Hold the object's lock, or a separate lock that covers the complete application invariant. Never hold a lock while performing unbounded I/O or waiting for another worker.

Release workers and update one counter safely
import multiprocessing as mp

def increment(ready, counter) -> None:
    if not ready.wait(timeout=5):
        return
    with counter.get_lock():
        counter.value += 1

def main() -> None:
    context = mp.get_context('spawn')
    ready = context.Event()
    counter = context.Value('i', 0)
    workers = [context.Process(target=increment, args=(ready, counter)) for _ in range(3)]
    for worker in workers:
        worker.start()
    ready.set()
    for worker in workers:
        worker.join(timeout=5)
    print(counter.value)

if __name__ == '__main__':
    main()
Output
3
Back to quick reference ↑
06

Assign one cleanup owner for every shared-memory block

SharedMemory exposes a named memoryview, avoiding repeated serialization and copies for large byte-oriented data. It supplies no application-level locking, type layout, bounds protocol, or ownership scheme; define those separately and never resize a buffer through one participant's assumptions. Every handle should close when finished, while exactly one owner calls unlink once after no participant can access the block. On Windows, unlink has no effect and the block disappears after all handles close. Python 3.13 added track: related multiprocessing children normally share one resource tracker and should keep its default. Independently launched processes get separate trackers, so track=False is appropriate only when another process or external system definitively owns cleanup.

Update a fixed-width integer through shared memory
import multiprocessing as mp
import struct
from multiprocessing.shared_memory import SharedMemory

def add_one(name: str) -> None:
    block = SharedMemory(name=name)
    try:
        current = struct.unpack_from('!i', block.buf, 0)[0]
        struct.pack_into('!i', block.buf, 0, current + 1)
    finally:
        block.close()

def main() -> None:
    context = mp.get_context('spawn')
    block = SharedMemory(create=True, size=4)
    try:
        struct.pack_into('!i', block.buf, 0, 41)
        process = context.Process(target=add_one, args=(block.name,))
        process.start()
        process.join(timeout=5)
        if process.exitcode != 0:
            raise RuntimeError(f'worker exit code: {process.exitcode}')
        print(struct.unpack_from('!i', block.buf, 0)[0])
    finally:
        block.close()
        block.unlink()

if __name__ == '__main__':
    main()
Output
42
Back to quick reference ↑
07

Use manager proxies for flexibility, not high-throughput updates

Manager() starts a server process that owns ordinary Python objects and exposes proxies to other processes. This supports flexible dictionaries, lists, sets in Python 3.14, namespaces, queues, and synchronization primitives—even across machines with a configured BaseManager—but every proxy operation is IPC and may serialize data. Batch work instead of issuing tiny remote operations in a loop. Nested ordinary mutable values are not automatically observed when edited in place; reassign the changed value or store a managed proxy inside the outer proxy. Protect a proxy shared by multiple threads, and close the manager context to shut down its server.

Collect named results through a managed dictionary
import multiprocessing as mp

def record(shared, key: str, value: int) -> None:
    shared[key] = value * value

def main() -> None:
    context = mp.get_context('spawn')
    with context.Manager() as manager:
        shared = manager.dict()
        workers = [context.Process(target=record, args=(shared, key, value)) for key, value in [('b', 3), ('a', 2)]]
        for worker in workers:
            worker.start()
        for worker in workers:
            worker.join(timeout=5)
        print(sorted(shared.items()))

if __name__ == '__main__':
    main()
Output
[('a', 4), ('b', 9)]
Back to quick reference ↑
08

Treat pickling as compatibility and trust boundaries

Spawn, forkserver, queues, pipes, pools, and managers serialize many callables and values with pickle. Define workers and custom classes at importable module scope; lambdas, nested functions, open files, locks from an incompatible context, and live network clients are unsuitable payloads. Send compact identifiers and immutable data rather than large object graphs or parent runtime state. Unpickling can execute attacker-chosen code, so never receive multiprocessing messages from an untrusted peer. Listener and Client can authenticate peers with HMAC, but authentication does not make pickle safe for an attacker holding the key and does not encrypt traffic.

Round-trip a simple immutable job description
import pickle
from dataclasses import dataclass

@dataclass(frozen=True)
class Job:
    item_id: int
    operation: str

job = Job(item_id=17, operation='normalize')
payload = pickle.dumps(job, protocol=pickle.HIGHEST_PROTOCOL)
restored = pickle.loads(payload)
print(restored == job)
print(restored.operation)
Output
True
normalize
Back to quick reference ↑
09

Design cooperative shutdown before forceful termination

A timeout reports that a process is still running; it does not cancel work. Prefer an Event, sentinel message, or finite input channel so the worker can release locks, close shared-memory handles, and flush results. terminate() skips finally blocks and exit handlers, can corrupt queues or pipes, and can leave acquired locks permanently blocking peers; kill() is even less graceful. Python 3.14 adds Process.interrupt() on POSIX, but its Windows behavior is undefined and workers may catch KeyboardInterrupt. If force is unavoidable, isolate that process from shared synchronization, terminate only after a grace period, join it to reap resources, and recreate any affected IPC. Drain Queue output before joining producers to avoid feeder-thread deadlocks.

Stop a worker cooperatively and verify its exit
import multiprocessing as mp

def wait_for_stop(stop) -> None:
    while not stop.wait(timeout=0.05):
        pass

def main() -> None:
    context = mp.get_context('spawn')
    stop = context.Event()
    process = context.Process(target=wait_for_stop, args=(stop,))
    process.start()
    stop.set()
    process.join(timeout=5)
    if process.is_alive():
        process.terminate()
        process.join()
    print(process.exitcode)

if __name__ == '__main__':
    main()
Output
0
Back to quick reference ↑

Local code tester

Plan deterministic process-pool chunks

Partition independent items into bounded chunks before submitting them to worker processes; this planning step runs without starting processes.

Runs in your browser
Output
Press Run to load Python locally.

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. Python Software Foundationmultiprocessing — Process-based parallelismdocs.python.org
  2. Python Software FoundationContexts and start methodsdocs.python.org
  3. Python Software Foundationmultiprocessing.shared_memory — Direct shared-memory accessdocs.python.org
  4. Python Software FoundationProgramming guidelines for multiprocessingdocs.python.org
  5. Python Software Foundationpickle — Python object serializationdocs.python.org
  6. Python Software FoundationPEP 371 — Addition of multiprocessing to the standard librarypeps.python.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback