Skip to content

multiprocessing Module Complexity

The multiprocessing module runs Python code in separate operating-system processes, each with its own interpreter and memory. Nothing is shared by default: apart from what a fork child inherits and the shared-memory objects, everything a process, pool, queue, pipe or manager carries crosses a pipe or socket as bytes, pickled unless it is sent with send_bytes().

So the size that governs most rows is the pickled size of what is sent, not the number of calls, and every call that can block also costs the time it spends blocked.

s is the time to start one process: a fork of the caller under the fork start method, a fork of an already running server under forkserver, a fresh interpreter under spawn. The last two also unpickle what they are sent, and the first forkserver start launches the server itself. w is time spent blocked, b is the pickled size of what an operation sends and receives, k is child processes started and not yet joined, n is items handed to a pool call, p is worker processes in a pool, m is one round trip to a manager's server process, f is the caller's own function, callback or constructor, z is bytes of shared memory, e is items in a ShareableList, v is processes or threads waiting on a primitive, d is how many times the caller holds a recursive lock, and r is types registered on a manager class. Pickling is priced per byte of output; a __reduce__ or __getstate__ the caller wrote is part of f. Under spawn and forkserver the first lock, semaphore or queue a process creates - Value(), Array() and Pool() each create one - starts the resource tracker process, once, at O(s); under every start method the first tracked SharedMemory or the first SharedMemoryManager does.

The rows describe POSIX systems. On Windows only spawn exists, terminate() and kill() both call TerminateProcess() and interrupt() is unavailable, there is no resource tracker, and a SharedMemory block lasts until its last handle closes, unlink() doing nothing.

Complexity Reference

Process

Operation Time Space Notes
multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None) O(a) O(a) a = arguments, which it copies; nothing runs, and active_children() does not list it, until start()
Process.start() O(k + s + b) O(k + b) Reaps finished children first; spawn and forkserver pickle the process object, target and arguments included, while fork pickles nothing
Process.run() O(f) O(f) Calls the target once, in whichever process calls run(); start() calls it in the child
Process.join(timeout=None) O(w) O(1) Immediate on a finished process; a timeout returns with the process still alive
Process.is_alive() O(1) O(1) One non-blocking poll
Process.terminate(), Process.kill(), Process.interrupt() O(1) O(1) Send SIGTERM, SIGKILL or SIGINT and return without waiting; join() waits for the exit. interrupt() is Python 3.14+
Process.close() O(1) O(1) Raises ValueError while the process is alive; afterwards is_alive(), join(), exitcode and pid raise it
Process.name, Process.daemon, Process.authkey, Process.pid, Process.ident, Process.exitcode, Process.sentinel O(1) O(1) pid, ident and exitcode are None until start(), and sentinel raises ValueError; exitcode polls the child

Contexts and module functions

Operation Time Space Notes
multiprocessing.current_process(), multiprocessing.parent_process() O(1) O(1) parent_process() is None in the main process
multiprocessing.active_children() O(k) O(k) Reaps finished children and returns a new list of the live ones
multiprocessing.cpu_count() O(1) O(1) os.cpu_count(), raising NotImplementedError where that is None
multiprocessing.get_context(method=None) O(1) O(1) The module's API bound to one start method; raises ValueError for an unknown one
multiprocessing.get_start_method(allow_none=False), multiprocessing.set_start_method(method, force=False) O(1) O(1) A second set_start_method() raises RuntimeError unless force=True
multiprocessing.get_all_start_methods() O(1) O(1) A new list each call
multiprocessing.get_logger(), multiprocessing.log_to_stderr(level=None) O(1) O(1) One logger, created on first use; every log_to_stderr() call adds another handler
multiprocessing.freeze_support(), multiprocessing.set_executable(executable), multiprocessing.allow_connection_pickling() O(1) O(1) Store a setting, or do nothing outside a frozen executable
multiprocessing.set_forkserver_preload(module_names) O(len(module_names)) O(1) Checks that each name is a str and keeps the list; the server imports them when it starts

Pool

Operation Time Space Notes
multiprocessing.Pool(processes=None, initializer=None, initargs=(), maxtasksperchild=None) O(p·(s + k)) O(p) Starts p workers, by default os.process_cpu_count() (os.cpu_count() before Python 3.13), and three handler threads
Pool.map(func, iterable, chunksize=None), Pool.starmap(func, iterable, chunksize=None) O(n + b + w) O(n + b) Blocks for every result and returns them in input order; an iterable without len() is listed first. Items go out in chunks of ⌈n / 4p⌉ by default and func is pickled once per chunk, so chunksize=1 pickles it n times
Pool.map_async(...), Pool.starmap_async(...) O(n) O(n) Lists an input without len() and allocates a slot per result in the caller, then returns; a handler thread chunks, pickles and sends the items
Pool.apply(func, args=(), kwds={}) O(b + w) O(b) One task, blocked on until its result is back
Pool.apply_async(func, args=(), kwds={}, callback=None, error_callback=None) O(1) O(1) Queues one task for the handler thread
Pool.imap(func, iterable, chunksize=1), Pool.imap_unordered(func, iterable, chunksize=1) O(1) O(1) Returns at once; a handler thread then reads the whole input as fast as the workers take it, whether or not anyone consumes the results. With the default chunksize of 1, func is pickled n times
Iterating an imap() or imap_unordered() result O(w) per item O(n + b) Results are unpickled and buffered as they arrive, consumed or not, so a slow consumer ends up holding all n; imap() also holds any that arrive ahead of their turn
Pool.close() O(1) O(1) Refuses new tasks; queued ones still run
Pool.join() O(w + p) O(1) Raises ValueError unless close() or terminate() came first
Pool.terminate() O(w + p + q + b) O(b) q = tasks still queued, which it discards; kills every worker and waits for it. with Pool() as pool: calls this on exit, so work still running is lost, not waited for
multiprocessing.pool.ThreadPool(processes=None, initializer=None, initargs=()) O(p) O(p) The same API on p threads of this process: tasks and results are passed by reference, so no row above pays b. A thread cannot be killed, so terminate() returns while a running task carries on

AsyncResult

Operation Time Space Notes
AsyncResult.get(timeout=None), AsyncResult.wait(timeout=None) O(w) O(1) The pool's result thread has already unpickled the result; get(timeout) raises multiprocessing.TimeoutError, which is not the builtin
AsyncResult.ready(), AsyncResult.successful() O(1) O(1) successful() raises ValueError until ready
callback, error_callback of the _async methods O(f) O(f) Run in a pool handler thread, normally the one delivering results, before ready() turns true, so a slow one holds up every result behind it

Queue, JoinableQueue and SimpleQueue

Operation Time Space Notes
multiprocessing.Queue(maxsize=0), multiprocessing.JoinableQueue(maxsize=0), multiprocessing.SimpleQueue() O(1) O(1) A pipe and its locks
Queue.put(obj, block=True, timeout=None), Queue.put_nowait(obj) O(w) O(1) Waits only for a slot in a full queue. A feeder thread pickles and writes the object later, so it is sent as it is then, and an unpicklable one fails in that thread, not in put()
Queue.get(block=True, timeout=None), Queue.get_nowait() O(w + b) O(b) Reads one message, then unpickles it after releasing the reader lock
Queue.qsize(), Queue.empty(), Queue.full() O(1) O(1) qsize() counts items put and not yet got, and raises NotImplementedError on macOS; empty() polls the pipe, so it stays true until the feeder has written
Queue.close(), Queue.cancel_join_thread() O(1) O(1) After close() the feeder still writes what is buffered, and a later put() or get() raises ValueError
Queue.join_thread() O(w) O(1) Waits for the feeder to flush the buffer; requires close() first
JoinableQueue.task_done() O(v + w) O(1) Wakes every join() when the count reaches zero; one call too many raises ValueError
JoinableQueue.join() O(w) O(1) Returns once every item put has had its task_done()
SimpleQueue.put(obj) O(b + w) O(b) Pickles in the caller, then writes under the writer lock
SimpleQueue.get() O(w + b) O(b)
SimpleQueue.empty(), SimpleQueue.close() O(1) O(1)

Pipe and Connection

Operation Time Space Notes
multiprocessing.Pipe(duplex=True) O(1) O(1) A socket pair, or a one-way pipe with duplex=False
Connection.send(obj), Connection.send_bytes(buf, offset=0, size=None) O(b + w) O(b) send() pickles in the caller; once the operating-system buffer is full, the call blocks until the other end reads
Connection.recv(), Connection.recv_bytes(maxlength=None) O(w + b) O(b) One whole message; one longer than maxlength raises OSError
Connection.recv_bytes_into(buf, offset=0) O(w + b) O(b) Too small a buffer raises BufferTooShort carrying the message
Connection.poll(timeout=0.0) O(w) O(1) Immediate by default
Connection.fileno(), Connection.close(), Connection.closed, Connection.readable, Connection.writable O(1) O(1)

Listener and Client

Operation Time Space Notes
multiprocessing.connection.Listener(address=None, family=None, backlog=1, authkey=None) O(1) O(1) Binds and listens on a socket, or a named pipe on Windows
Listener.accept() O(w) O(1) Waits for a client; with an authkey, the two sides then run deliver_challenge() and answer_challenge()
Listener.close(), Listener.address, Listener.last_accepted O(1) O(1) last_accepted is None until a client connects
multiprocessing.connection.Client(address, family=None, authkey=None) O(w) O(1) Connects and authenticates as accept() does; with no authkey on either side, nothing is checked
multiprocessing.connection.wait(object_list, timeout=None) O(len(object_list) + w) O(len(object_list)) Registers every object on each call and returns a new list of the ready ones, empty after a timeout
multiprocessing.connection.deliver_challenge(connection, authkey), multiprocessing.connection.answer_challenge(connection, authkey) O(w) O(1) An HMAC exchange of fixed-size messages; a mismatched key raises AuthenticationError on both sides

Lock, RLock, Semaphore and BoundedSemaphore

Operation Time Space Notes
multiprocessing.Lock(), multiprocessing.RLock(), multiprocessing.Semaphore(value=1), multiprocessing.BoundedSemaphore(value=1) O(1) O(1) One named operating-system semaphore each
Lock.acquire(block=True, timeout=None), RLock.acquire(block=True, timeout=None) O(w) O(1) Immediate when free, and for an RLock its owner
Lock.release(), RLock.release() O(1) O(1) An unheld Lock raises ValueError, an unheld RLock AssertionError
Lock.locked(), RLock.locked(), Semaphore.locked() O(1) O(1) Python 3.14+
Semaphore.acquire(block=True, timeout=None) O(w) O(1) Immediate while the count is above zero
Semaphore.release() O(1) O(1) BoundedSemaphore raises ValueError past its initial value; on macOS it checks only an initial value of 1
Semaphore.get_value() O(1) O(1) Raises NotImplementedError on macOS

Condition

Operation Time Space Notes
multiprocessing.Condition(lock=None) O(1) O(1) A new RLock unless given a lock
Condition.acquire(), Condition.release() O(w) O(1) The underlying lock's
Condition.wait(timeout=None) O(w + d) O(1) Releases a lock held d times all the way, waits, and takes it back d times
Condition.wait_for(predicate, timeout=None) O(w + c·(f + d)) O(f) c = predicate calls, one before waiting and one per wakeup
Condition.notify(n=1) O(u + t + w) O(1) u = sleepers it wakes, t = waits that timed out since the last notify, whose bookkeeping it clears first. It waits for every woken sleeper to acknowledge, so a sleeper killed mid-wait blocks it for good
Condition.notify_all() O(v + t + w) O(1) notify() for every sleeper

Event

Operation Time Space Notes
multiprocessing.Event() O(1) O(1)
Event.set() O(v + t + w) O(1) Wakes every waiter through notify_all(), killed-sleeper hang included
Event.clear(), Event.is_set() O(w) O(1) Both take the event's lock
Event.wait(timeout=None) O(w) O(1) Immediate once set

Barrier

Operation Time Space Notes
multiprocessing.Barrier(parties, action=None, timeout=None) O(1) O(1)
Barrier.wait(timeout=None) O(w + v + f) O(f) The last of the parties runs action, f, and wakes the others; a timeout that expires breaks the barrier
Barrier.reset(), Barrier.abort() O(v + w) O(1) Every waiter raises BrokenBarrierError; abort() leaves the barrier broken, reset() does not
Barrier.parties, Barrier.n_waiting, Barrier.broken O(1) O(1)

Value and Array

Operation Time Space Notes
multiprocessing.RawValue(typecode_or_type, *args), multiprocessing.Value(typecode_or_type, *args, lock=True) O(z) O(z) z = the type's size, zeroed and then initialised; Value() wraps it with an RLock, or returns the raw ctypes object with lock=False
multiprocessing.RawArray(typecode_or_type, size_or_initializer), multiprocessing.Array(typecode_or_type, size_or_initializer, *, lock=True) O(z) O(z) z = the whole array, zeroed or copied from the sequence. A child given it through Process() shares the memory rather than copying it; as a pool task argument, this and every object in this table raises RuntimeError
multiprocessing.sharedctypes.synchronized(obj, lock=None, ctx=None) O(1) O(1) The wrapper Value() and Array() return, put around an existing ctypes object without copying it; the wrapper class for a Structure type is built once, costing its field count
Value.value O(w) O(1) Under the wrapper's lock; a c_char array's value is a new bytes object, O(z)
Array[i], Array[i] = x O(w) O(1) Under the lock
Array[i:j] O(w + j − i) O(j − i) A new list, or bytes for a 'c' array and str for a 'u' array, under the lock
len(Array) O(1) O(1) Takes no lock
Value.get_lock(), Value.get_obj(), Value.acquire(), Value.release() O(w) O(1) The wrapper's lock and raw object; only acquire() waits, and with value: holds the lock

SharedMemory

Operation Time Space Notes
multiprocessing.shared_memory.SharedMemory(name=None, create=False, size=0, *, track=True) O(1) O(z) z = size; creating it and attaching to it by name both register the block with the resource tracker, which unlinks any block still registered when the program exits; track=False (Python 3.13+) opts out
SharedMemory.buf O(1) O(1) The same memoryview on every access; slicing it copies nothing
SharedMemory.name, SharedMemory.size O(1) O(1)
SharedMemory.close() O(1) O(1) Unmaps this process's view, leaving the block; raises BufferError while a slice of buf is still alive
SharedMemory.unlink() O(1) O(1) Destroys the block; call it once, from one process

ShareableList

Operation Time Space Notes
multiprocessing.shared_memory.ShareableList(sequence=None, *, name=None) O(z) O(z) Packs every item into a new block of z bytes, which grows with the item count and the str and bytes lengths; attaching by name is O(e)
ShareableList[i], ShareableList[i] = x O(1) O(1) A str or bytes slot costs its size, fixed when the list is built at the next multiple of 8 above the item's encoded length; a longer value raises ValueError. The list's length is fixed
len(ShareableList) O(1) O(1)
ShareableList.count(value), ShareableList.index(value) O(z) O(1) Read the items one at a time, index() up to the first match
ShareableList.format O(e) O(e) A new string of every item's struct format
ShareableList.shm O(1) O(1) The SharedMemory holding the list

SyncManager

Operation Time Space Notes
multiprocessing.Manager() O(k + s + m) O(k) Starts a SyncManager's server process and waits for its address
SyncManager.list(), SyncManager.dict(), SyncManager.set(), SyncManager.Namespace(), SyncManager.Value(), SyncManager.Array(), SyncManager.Queue(), SyncManager.JoinableQueue(), SyncManager.Lock(), SyncManager.RLock(), SyncManager.Semaphore(), SyncManager.BoundedSemaphore(), SyncManager.Condition(), SyncManager.Event(), SyncManager.Barrier(), SyncManager.Pool() O(m + b + f) O(b) Builds the object in the server, at its constructor's cost f, and returns a proxy; initial contents cross as b bytes. set() is Python 3.14+
Calling a proxy method O(m + b + f) O(b) Every call is a round trip, and a thread's first call also opens its connection. Iterating a list or dict proxy costs a round trip per element; proxy[:] on a list and copy(), keys(), values() or items() on a dict copy it in one

BaseManager

Operation Time Space Notes
multiprocessing.managers.BaseManager(address=None, authkey=None, serializer='pickle', ...) O(1) O(1) Nothing starts until start() or connect()
BaseManager.start(initializer=None, initargs=()) O(k + s + m + b) O(k + b) Starts the server process; b = the registry, initializer and arguments, pickled under spawn and forkserver. A second call raises ProcessError; with manager: shuts it down on exit
BaseManager.connect() O(m) O(1) Reaches a server another process started; nothing is spawned
BaseManager.get_server() O(1) O(1) A server to run in this process with serve_forever(); raises ProcessError once started
BaseManager.shutdown(), BaseManager.join(timeout=None) O(w) O(1) shutdown() exists only once started
BaseManager.register(typeid, callable=None, proxytype=None, ...) O(r) O(r) A classmethod: the first call on a subclass copies the r registrations it inherits, and later ones are O(1)
BaseManager.address O(1) O(1)

BaseProxy

Operation Time Space Notes
BaseProxy._callmethod(methodname, args=(), kwds={}) O(m + b + f) O(b) The round trip behind every proxy method
BaseProxy._getvalue() O(m + b) O(b) One round trip returning a copy of the referent

SharedMemoryManager

Operation Time Space Notes
multiprocessing.managers.SharedMemoryManager(address=None, authkey=None) O(1) O(1) A BaseManager; start() costs what it does there
SharedMemoryManager.SharedMemory(size) O(m) O(z) Creates the block in the caller and registers it with the server in one round trip
SharedMemoryManager.ShareableList(sequence) O(m + z) O(z) The same, for a ShareableList
SharedMemoryManager.shutdown() O(g² + w) O(g) g = blocks it registered; it unlinks each one and removes its name from the front of a list

multiprocessing.dummy

Operation Time Space Notes
multiprocessing.dummy O(1) O(1) The same API on threads: Process is a threading.Thread, Queue is queue.Queue, Pool() is a ThreadPool, Pipe() passes objects by reference and Manager() returns the module itself. Nothing is pickled, so b drops out of every bound

Constants and exceptions

Operation Time Space Notes
multiprocessing.ProcessError, multiprocessing.BufferTooShort, multiprocessing.TimeoutError, multiprocessing.AuthenticationError O(1) O(1) The last three subclass ProcessError
multiprocessing.SUBDEBUG, multiprocessing.SUBWARNING O(1) O(1) Logging levels 5 and 25
multiprocessing.reducer O(1) O(1) The reduction module, whose ForkingPickler pickles every transfer

Starting Processes

Starting a process costs a fork or a new interpreter, s, and under spawn and forkserver it also pickles the target and its arguments. join() costs only the wait.

from multiprocessing import Pipe, Process, current_process

def report(conn, n):
    conn.send((current_process().name, n * n))
    conn.close()

if __name__ == "__main__":
    parent_end, child_end = Pipe()
    p = Process(target=report, args=(child_end, 7), name="worker")  # O(a) - nothing runs yet
    assert p.pid is None
    p.start()  # O(k + s + b) - under spawn or forkserver the args are pickled
    assert parent_end.recv() == ("worker", 49)
    p.join()  # O(w)
    assert p.exitcode == 0

Start Methods

The if __name__ == "__main__": guard is required under spawn and forkserver, which import the main module in a fresh interpreter: every child under spawn, the server under forkserver. One of the two is the default everywhere from Python 3.14, and on macOS and Windows before it. A context pins a start method without changing the global one.

import multiprocessing

def square(n):
    return n * n

if __name__ == "__main__":
    methods = multiprocessing.get_all_start_methods()  # O(1)
    assert "spawn" in methods
    context = multiprocessing.get_context("spawn")  # O(1)
    with context.Pool(1) as pool:  # O(p·(s + k)) - spawn starts a fresh interpreter
        assert pool.apply(square, (6,)) == 36  # O(b + w)
    assert context.get_start_method() == "spawn"

Pools

Chunking

map() pickles the function once per chunk. With the default chunk size that is about 4p times; with chunksize=1 it is n times, so a function that is expensive to pickle, such as a bound method of a large object or a functools.partial holding one, pays for that on every item. imap() and imap_unordered() default to chunksize=1.

from multiprocessing import Pool

def square(n):
    return n * n

if __name__ == "__main__":
    with Pool(processes=2) as pool:  # O(p·(s + k))
        results = pool.map(square, range(1000))  # O(n + b + w) - 8 chunks of 125
        assert sum(results) == 332833500
        chunked = pool.imap(square, range(1000), chunksize=100)  # O(1) - 10 chunks
        assert next(chunked) == 0  # O(w) per item
        assert sum(chunked) == 332833500

imap and Backpressure

imap() returns without waiting for a result, but a handler thread then feeds the whole input to the workers, and the results pile up in the iterator whether or not anything consumes them. It saves the wait for the first result, not the memory for the rest.

import time
from multiprocessing import Pool

def identity(n):
    return n

if __name__ == "__main__":
    consumed = []

    def numbers():
        for n in range(100):
            consumed.append(n)
            yield n

    with Pool(2) as pool:
        results = pool.imap(identity, numbers())  # O(1) - returns at once
        deadline = time.monotonic() + 10
        while len(consumed) < 100 and time.monotonic() < deadline:
            time.sleep(0.01)
        assert len(consumed) == 100  # read to the end with no next() called
        assert list(results) == list(range(100))

Threads Instead of Processes

A ThreadPool has the same API and pickles nothing, so it suits work that releases the GIL, such as I/O. Its results are the objects the function returned.

from multiprocessing.pool import ThreadPool

def first(items):
    return items[0]

if __name__ == "__main__":
    shared = [object()]
    with ThreadPool(2) as pool:  # O(p) - threads, not processes
        (result,) = pool.map(first, [shared])  # no b term - nothing is pickled
    assert result is shared[0]

Queues and Pipes

Queue.put() hands the object to a feeder thread and returns; the pickling happens later, in that thread. Connection.send() and SimpleQueue.put() pickle in the caller.

from multiprocessing import Process, Queue

def produce(queue, items):
    for item in items:
        queue.put(item)  # O(w) - the feeder thread pickles it
    queue.put(None)

if __name__ == "__main__":
    queue = Queue()
    producer = Process(target=produce, args=(queue, range(5)))
    producer.start()
    received = []
    while (item := queue.get()) is not None:  # O(w + b) each
        received.append(item)
    producer.join()
    assert received == [0, 1, 2, 3, 4]

A send() larger than the operating-system buffer blocks until the other end reads, so two processes that each send a large message before they receive deadlock.

from multiprocessing import Pipe, Process

def respond(conn):
    request = conn.recv()  # O(w + b)
    conn.send(request.upper())  # O(b + w)
    conn.close()

if __name__ == "__main__":
    parent_end, child_end = Pipe()  # O(1)
    worker = Process(target=respond, args=(child_end,))
    worker.start()
    parent_end.send("ping")  # O(b + w) - pickled here, in the caller
    assert parent_end.recv() == "PING"
    worker.join()

Sharing State

Value and Array

A Value or Array lives in shared memory, so a child writes into the parent's copy. Each read and each write takes the wrapper's lock separately, so counter.value += 1 takes the lock twice and releases it in between; hold get_lock() across a read-modify-write.

from multiprocessing import Array, Process, Value

def fill(counter, cells):
    with counter.get_lock():  # O(w)
        counter.value += 1  # O(1) - read and write under the held lock
    for i in range(len(cells)):  # O(z) - written in place, no copy
        cells[i] = i * 10

if __name__ == "__main__":
    counter = Value("i", 0)  # O(z)
    cells = Array("i", 5)  # O(z) - zeroed shared memory
    child = Process(target=fill, args=(counter, cells))
    child.start()
    child.join()
    assert counter.value == 1
    assert cells[:] == [0, 10, 20, 30, 40]  # O(w + j − i)

SharedMemory and ShareableList

A SharedMemory block is raw bytes that any process can attach to by name, at O(1) whatever its size. A ShareableList packs numbers, str, bytes and None into one; its length is fixed and a str or bytes item cannot outgrow the slot it was built with.

from multiprocessing import shared_memory

block = shared_memory.SharedMemory(create=True, size=1024)  # O(1) time, O(z) memory
try:
    block.buf[:5] = b"hello"  # O(5) - a write into the shared bytes
    attached = shared_memory.SharedMemory(name=block.name)  # O(1) - no copy
    assert bytes(attached.buf[:5]) == b"hello"
    attached.close()
finally:
    block.close()
    block.unlink()  # once, from one process

items = shared_memory.ShareableList([1, 2.5, "text", None])  # O(z)
try:
    items[0] = 42  # O(1)
    assert items[0] == 42 and len(items) == 4
    assert items.index(None) == 3  # O(z) - reads item by item
    try:
        items[2] = "x" * 100
    except ValueError as error:
        assert "exceeds available storage" in str(error)
    else:
        raise AssertionError("a str grew past its allocation")
finally:
    items.shm.close()
    items.shm.unlink()

Managers

A manager keeps the object in its own server process, and every proxy operation is a round trip to it. Copy a whole list or dict in one call rather than iterating the proxy.

from multiprocessing import Manager

if __name__ == "__main__":
    with Manager() as manager:  # O(k + s + m)
        shared = manager.list(range(5))  # O(m + b)
        shared.append(5)  # O(m + b) - one round trip
        copied = shared[:]  # O(m + b) - one round trip for the whole list
        doubled = [x * 2 for x in shared]  # O(m) per element - a round trip each
        assert copied == [0, 1, 2, 3, 4, 5]
        assert doubled == [0, 2, 4, 6, 8, 10]

Connecting Unrelated Processes

Listener and Client give two processes that did not start one another a Connection. With an authkey on both ends, each connection runs a fixed-size challenge before any data moves.

from multiprocessing import Process
from multiprocessing.connection import Client, Listener

def call(address):
    with Client(address, authkey=b"secret") as conn:  # O(w) - connect and authenticate
        conn.send([1, 2, 3])  # O(b + w)
        assert conn.recv() == 6

if __name__ == "__main__":
    with Listener(authkey=b"secret") as listener:  # O(1)
        caller = Process(target=call, args=(listener.address,))
        caller.start()
        with listener.accept() as conn:  # O(w)
            conn.send(sum(conn.recv()))  # O(w + b)
        caller.join()
    assert caller.exitcode == 0

Common Patterns

Fan Out, Collect as Completed

imap_unordered() with an explicit chunk size pickles the function once per chunk and hands back each chunk's results as soon as that chunk finishes, whatever the order, so the parent works on early results while slow chunks are still running.

from multiprocessing import Pool

def score(word):
    return word, sum(map(ord, word))

if __name__ == "__main__":
    words = ["alpha", "beta", "gamma", "delta"] * 250
    totals = {}
    with Pool(2) as pool:
        for word, value in pool.imap_unordered(score, words, chunksize=50):  # O(w) each
            totals[word] = value  # O(1) amortized
    assert totals == {word: sum(map(ord, word)) for word in set(words)}

Performance Best Practices

Do:

  • Pass chunksize to imap() and imap_unordered(), whose default of 1 pickles the function once per item
  • Send small arguments and results; b is paid in both directions on every task
  • Copy a manager list or dict in one call (proxy[:], copy(), items()) instead of iterating the proxy
  • Hold a Value's lock across a read-modify-write
  • Put large data every pool task reads in SharedMemory and pass the block, which pickles as its name
  • Close a Pool and join() it when the queued work must finish; the with block terminates it

Avoid:

  • imap() over a huge or endless input as a way to bound memory; the pool reads it all and buffers every result
  • Iterating a manager proxy in a loop; each element is a round trip
  • Slow callbacks on the _async methods; they hold up every later result
  • Two processes that both send large messages before either receives
  • A process Pool for I/O-bound work; a ThreadPool pickles nothing

Version Notes

  • Python 3.12+: The fork start method emits a DeprecationWarning when the parent has more than one thread
  • Python 3.13+: Pool() defaults to os.process_cpu_count() workers; SharedMemory takes track=False
  • Python 3.14+: The default start method on POSIX other than macOS is forkserver, not fork; Process.interrupt(), Lock.locked(), RLock.locked(), Semaphore.locked() and SyncManager.set() added
  • concurrent.futures - ProcessPoolExecutor, a process pool behind a futures interface
  • threading - the thread primitives multiprocessing.dummy and ThreadPool are built on
  • queue - the in-process queues multiprocessing.dummy uses
  • pickle - what every transfer costs
  • asyncio - concurrency without processes or threads