Skip to main content

Python Shared Memory

Summary
Master Python multiprocessing.shared_memory for zero-copy IPC. Learn synchronization, NumPy integration, and race condition prevention patterns.

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.

Array
ProducerConsumerfull array · 10 MBpicklecopy10 MB cross · 1.3 ms
10 MB
Queue · 1.3 ms
12 B
shared mem · 0.018 ms
at 10 MB: 833,345× fewer bytes cross, 72× faster handoff
bytes across the boundary vs array size (MB) — log scale
━ Queue scales with the array · ━ shared memory flat at 12 B
1. 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.

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.

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
No synchronization
Problem
Two processes can read the same old value and overwrite each other.
Fix
Use a Lock/RLock/Condition, or partition memory so writers never touch the same bytes.
Pitfall
Leaking blocks
Problem
A POSIX shared memory block can outlive the creating process.
Fix
Call close() in every process and unlink() once when the block is no longer needed.
Pitfall
Independent resource trackers
Problem
Standalone Python processes can each create a tracker; the first exit may unlink the block.
Fix
Use SharedMemoryManager, a single owner process, or track=False when another owner handles cleanup.
Pitfall
Wrong byte size
Problem
The NumPy shape and dtype may require more bytes than the block contains.
Fix
Allocate array.nbytes or prod(shape) * dtype.itemsize and pass shape/dtype with the name.
Pitfall
Using the buffer after close
Problem
The memoryview becomes invalid for that process.
Fix
Copy any result you need before closing the SharedMemory handle.

API reference

API
SharedMemory(...)
Purpose
Create or attach to a named shared byte block.
Detail
Python 3.13 signature is SharedMemory(name=None, create=False, size=0, *, track=True).
API
shm.buf
Purpose
memoryview over the shared bytes.
Detail
Use explicit slicing and encoding/decoding; do not access after close().
API
shm.name
Purpose
Generated or user-provided identifier for the block.
Detail
Pass this value to other processes so they can attach to the same bytes.
API
shm.size
Purpose
Total bytes available through this handle.
Detail
May be rounded up by the operating system; still size arrays explicitly.
API
shm.close()
Purpose
Release this process mapping/handle.
Detail
Call it in every process when that process is done with the block.
API
shm.unlink()
Purpose
Request deletion of the shared memory block.
Detail
Call once per block. On Windows, deletion happens when all handles close.
API
SharedMemoryManager
Purpose
Own shared blocks from a manager process.
Detail
Use it when manual lifetime ownership would be fragile.

If you found this explanation helpful, consider sharing it with others.

Mastodon