What shared memory changes
Normal multiprocessing sends data between isolated address spaces: a Queue or Pipe pickles the object, copies the bytes through an IPC channel, and rebuilds it on the other side. multiprocessing.shared_memory changes the data path — the OS owns one named byte region and every process maps that same region into its own address space, so reads and writes land on the same underlying bytes. The explorer below measures exactly what that buys.
By value versus shared memory
Moving a NumPy array between processes, using recorded CPython and NumPy measurements. A Queue pickles and copies the buffer; shared memory passes a short name and metadata to read the same bytes.
- Queue · 1.3 ms
- 10 MB
- Shared memory · 0.018 ms
- 12 B
At 10 MB: 833,345× fewer payload bytes and 72× lower measured attach time than pickle dumps plus loads. See timing boundaries below.
Bytes across the boundary versus array size (MB), log scale
Queue scales with the array; shared-memory name stays at 12 B.
Passing an array by value
A multiprocessing Queue or Pipe sends objects by pickling them. The whole array is serialized, copied across, and rebuilt on the other side — here a 10 MB array costs the full 10 MB across the boundary, paid again for every consumer.
Shared bytes still need ownership
Shared memory skips serialization and copying for the payload, but it does not make Python objects shared: you decide the byte layout, synchronize writes, and own the cleanup.
The three patterns below cover layout, synchronized writes, and cleanup.
Share a NumPy array
A SharedMemory block is raw bytes, and a NumPy ndarray can use shm.buf as its storage directly. The name alone is not enough — pass the shape and dtype alongside it, and size the block with array.nbytes:
import numpy as np
from multiprocessing import shared_memory
arr = np.arange(12, dtype=np.float64).reshape(3, 4)
shm = shared_memory.SharedMemory(create=True, size=arr.nbytes)
shared = np.ndarray(arr.shape, dtype=arr.dtype, buffer=shm.buf)
shared[:] = arr # write once
# another process, handed only shm.name + shape + dtype:
attached = shared_memory.SharedMemory(name=shm.name)
view = np.ndarray(arr.shape, dtype=arr.dtype, buffer=attached.buf) # zero-copy
shared and view are different Python objects over the same buffer. Every attached process calls close() on its own handle; exactly one owner calls unlink() to delete the block.
Write safely
Shared memory shares bytes, not atomic operations. A read-modify-write has three phases — read, compute, write back — and if two processes interleave them, an update vanishes. Wrap the full critical section in a lock:
from multiprocessing import Lock
lock = Lock()
def safe_increment(shm, lock):
with lock: # the whole read-modify-write
v = int.from_bytes(shm.buf[0:4], "little")
shm.buf[0:4] = (v + 1).to_bytes(4, "little")
For bulk numerical work, partitioning is faster than locking: give each worker a non-overlapping slice (image[start:stop]) so no two processes ever write the same bytes, and drop the lock entirely.
Lifetime: close vs. unlink
close() releases this process's mapping; unlink() deletes the block and must be called once, by the owner. Scatter that responsibility and you get either a leak (no one unlinks) or a use-after-free (someone unlinks early). When ownership is messy, let a manager own it:
from multiprocessing.managers import SharedMemoryManager
with SharedMemoryManager() as smm:
shm = smm.SharedMemory(size=1024) # the manager unlinks on context exit
...
Python 3.13 added track= to SharedMemory: track=True registers the block with the resource tracker (right within a multiprocessing family), but independent process trees each get their own tracker — there, use a single owner, a SharedMemoryManager, or track=False. On Windows track is ignored, since the OS deletes the block once all handles close.
Reference
Common pitfalls
| Pitfall | Problem | Fix |
|---|---|---|
| No synchronization | Two processes can read the same old value and overwrite each other. | Use a Lock/RLock/Condition, or partition memory so writers never touch the same bytes. |
| Leaking blocks | A POSIX shared memory block can outlive the creating process. | Call close() in every process and unlink() once when the block is no longer needed. |
| Independent resource trackers | Standalone Python processes can each create a tracker; the first exit may unlink the block. | Use SharedMemoryManager, a single owner process, or track=False when another owner handles cleanup. |
| Wrong byte size | The NumPy shape and dtype may require more bytes than the block contains. | Allocate array.nbytes or prod(shape) * dtype.itemsize and pass shape/dtype with the name. |
| Using the buffer after close | The memoryview becomes invalid for that process. | Copy any result you need before closing the SharedMemory handle. |
API reference
| API | Purpose | Detail |
|---|---|---|
| SharedMemory(...) | Create or attach to a named shared byte block. | Python 3.13 signature is SharedMemory(name=None, create=False, size=0, *, track=True). |
| shm.buf | memoryview over the shared bytes. | Use explicit slicing and encoding/decoding; do not access after close(). |
| shm.name | Generated or user-provided identifier for the block. | Pass this value to other processes so they can attach to the same bytes. |
| shm.size | Total bytes available through this handle. | May be rounded up by the operating system; still size arrays explicitly. |
| shm.close() | Release this process mapping/handle. | Call it in every process when that process is done with the block. |
| shm.unlink() | Request deletion of the shared memory block. | Call once per block. On Windows, deletion happens when all handles close. |
| SharedMemoryManager | Own shared blocks from a manager process. | Use it when manual lifetime ownership would be fragile. |
Related concepts
Deep dive into Python's asyncio library, understanding event loops, coroutines, tasks, and async/await patterns with interactive visualizations.
Follow CPython thread states, execution frames and shared objects through a GIL handoff. Inspect the source behind waiting, release, native work and application locks.
Complete guide to Python concurrency — OS threads, green threads (asyncio), the GIL, event loop internals, Python 3.13 free-threading, and production patterns.
Follow direct-style and CPS factorial through the same calculation. See how closures carry pending work, why Python still grows call depth, and where trampolines fit.
Complete C++ thread safety guide — race conditions with step-through simulation, mutexes, atomics, condition variables, deadlock detection, memory ordering, and Thread Sanitizer walkthrough.
Trace a small virtual machine through branches and loops, then compare switch, computed goto, and tail-call dispatch without changing its bytecode semantics.
