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 vs shared memory — measured
Moving a NumPy array between processes, measured on this machine. A Queue pickles and copies the whole buffer; shared memory hands off a ~12-byte name and reads the same bytes zero-copy.
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.
The trade is sharp. Shared memory skips serialization and copying for the payload, but it does not make Python objects magically shared: you decide the byte layout, synchronize writes, and own the cleanup. The three patterns below are the ones that matter.
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.
Learn the CPython Global Interpreter Lock (GIL) from first principles: why it exists, how threads take turns, why I/O still works well, and when to use multiprocessing, asyncio, or native extensions.
Complete guide to Python concurrency — OS threads, green threads (asyncio), the GIL, event loop internals, Python 3.13 free-threading, and production patterns.
Explore CPython bytecode compilation from source to .pyc files. Learn the dis module, PVM stack operations, and Python 3.11+ adaptive specialization.
Understand CPython garbage collection: reference counting, generational GC for circular references, weak references, and gc module tuning strategies.
A practical mental model for CPython memory management: names and references, object headers, PyMalloc arenas, reference counting, reuse paths, and memory profiling.
