Message-passing copies every byte. When the data is huge or the counter is hot, copying is the bottleneck. These are the escape hatches that let processes see the same bytes.
Pattern 3 gave you message passing โ safe, simple, and it copies. Send a 2 GB NumPy array to four workers and you've pickled and copied 8 GB. Increment a shared counter through a queue and every tick is a round-trip. For big buffers or hot shared state, the copy costs more than the computation.
Shared memory fixes this: the OS maps the same physical bytes into multiple processes. There's one copy of the data, and every process reads and writes it directly. The catch is that you've now re-introduced the thing processes were protecting you from โ shared mutable state โ so you're back to needing locks (Pattern 6).
Value and Array: Shared Scalars and Buffers
mp.Value and mp.Array are ctypes-backed primitives that live in
shared memory. You declare a C type code up front: Value('i', 0) is a shared 32-bit int,
Array('d', [1.0, 2.0, 3.0]) is a shared array of doubles. You read and write through the
.value attribute (for Value) or by indexing (for Array). Because
they're plain C types, they're fast and involve no pickling.
counter.value += 1 is a read, an add, and a write โ three steps.
Two processes can interleave and lose updates, exactly like the threading race. Shared memory does
not make operations atomic.
Value/Array ships with a built-in lock, reachable
via .get_lock(). Wrap read-modify-write sequences in it. Use the lock as a context manager.
import multiprocessing as mp
def increment(counter, times):
for _ in range(times):
with counter.get_lock(): # MANDATORY โ += is not atomic
counter.value += 1
if __name__ == "__main__":
counter = mp.Value('i', 0) # shared 32-bit int, starts at 0
workers = [
mp.Process(target=increment, args=(counter, 100_000))
for _ in range(4)
]
for w in workers:
w.start()
for w in workers:
w.join()
print(counter.value) # 400000 โ exact, because of the lock
Drop the with counter.get_lock(): and you'll get some number less than 400000, and a
different one on every run. See Pattern 6 for why the lock is
non-negotiable and how cross-process locks differ from threading ones.
shared_memory.SharedMemory: Zero-Copy Buffers (3.8+)
multiprocessing.shared_memory.SharedMemory is a raw, named block of shared
memory โ just bytes, addressable by a string name. It's the way to share a large NumPy array with
zero copy: you allocate a block, wrap it with a NumPy view via the
buffer= argument, and then pass only the block's name (a small string) to the
workers. Each worker attaches to the same block by that name and sees the same bytes โ no pickling of the
array itself.
import numpy as np
from multiprocessing import Process
from multiprocessing.shared_memory import SharedMemory
def worker(name, shape, dtype):
shm = SharedMemory(name=name) # ATTACH by name (no copy)
arr = np.ndarray(shape, dtype=dtype, buffer=shm.buf)
arr[:] = arr * 2 # mutate the shared bytes in place
shm.close() # detach THIS process's mapping
if __name__ == "__main__":
data = np.arange(10, dtype=np.int64)
shm = SharedMemory(create=True, size=data.nbytes) # allocate the block
# wrap the block as a NumPy array and fill it
shared = np.ndarray(data.shape, dtype=data.dtype, buffer=shm.buf)
shared[:] = data[:]
p = Process(target=worker, args=(shm.name, data.shape, data.dtype))
p.start()
p.join()
print(shared) # [0 2 4 6 8 10 12 14 16 18] โ worker's writes are visible
shm.close() # detach the parent's mapping
shm.unlink() # EXACTLY ONE process frees the block (see gotcha)
Note what crossed the process boundary: just shm.name, data.shape, and
data.dtype โ a few dozen bytes. The array itself never got pickled.
SharedMemory Won't Free ItselfSharedMemory block is an OS-level
resource that outlives the Python objects pointing at it. Every process that attached must call
close() to detach, and exactly one process must call
unlink() to actually release the underlying block. Forget unlink() and the memory
stays allocated after your program exits, and you'll see
resource_tracker: ... leaked shared_memory objects warnings.
close() โ call in every process, detaches that process's mapping.unlink() โ call in exactly one process (usually the creator/owner), frees the block.unlink() twice, and never unlink() while another process still needs the data โ it invalidates the block for everyone.close() is "I'm done looking at it"; unlink() is
"delete the file." Files (and shared-memory blocks) survive after readers close them โ someone has to
delete them. Wrap the owner's close()/unlink() in a try/finally so a
crash mid-computation still frees the block.
Manager: Shared Objects via a Server Process
A multiprocessing.Manager spins up a separate server process that holds real
Python objects. Children get proxies โ objects that look like a dict,
list, or Namespace but forward every operation to the server over IPC. This is
the convenient option: you can share arbitrary, nested Python objects, not just C scalars and raw bytes.
import multiprocessing as mp
def worker(shared_dict, key):
shared_dict[key] = key * key # each op is an IPC round-trip to the server
if __name__ == "__main__":
with mp.Manager() as manager:
shared_dict = manager.dict() # a PROXY backed by the server process
workers = [
mp.Process(target=worker, args=(shared_dict, n))
for n in range(5)
]
for w in workers:
w.start()
for w in workers:
w.join()
print(dict(shared_dict)) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
Value/shared_memory. Worse, mutating a
nested object doesn't propagate. d[k].append(x) mutates a local copy the
proxy handed you; the server never hears about it. You must reassign so the proxy re-sends the whole value:
# โ silently lost โ mutates a local copy, server never updated
shared_dict[k].append(x)
# โ
reassign so the proxy ships the new value back to the server
shared_dict[k] = shared_dict[k] + [x]
| Tool | Best for | Speed | Lifecycle | Shares |
|---|---|---|---|---|
Value / Array | Simple scalars & flat numeric arrays, hot counters | โ Fast | โ Automatic | C-typed primitives |
shared_memory | Big raw / NumPy buffers, zero-copy | โ โ Fastest | โ Manual (close + unlink) | Raw bytes |
Manager | Arbitrary / nested Python objects, convenience | โ Slow (IPC per op) | โ Automatic | Any picklable object |
Value/shared_memory/Manager only when
profiling shows the copy is the bottleneck. Shared mutable state buys you speed at the
cost of every concurrency bug processes were supposed to spare you.
shared_memory is a large, mostly
read-only dataset (a model, a lookup table, a feature matrix) that many CPU-bound workers scan in
parallel โ no writes means no locks, and you skip gigabytes of copying. See
Pattern 10 for measuring whether that copy is actually your
bottleneck before you take on the manual lifecycle.