How multiprocessing.sharedctypes Works in Python
The multiprocessing.sharedctypes module allows multiple
Python processes to access and manipulate the same C-compatible
primitive types and arrays directly in memory. By allocating raw ctypes
objects from shared memory segments instead of standard process-private
heap memory, the module enables zero-copy inter-process communication
without the overhead of serialization. This article details the
underlying mechanisms, memory allocation strategies, synchronization
wrappers, and practical implementation patterns that make
sharedctypes work across process boundaries.
Shared Memory Allocation
Under the hood, multiprocessing.sharedctypes relies on
the operating system's shared memory facilities. In Unix-like
environments, it typically uses anonymous memory mappings
(mmap) or shared memory objects (shm_open),
whereas on Windows, it creates named file mapping objects backed by the
system paging file.
When a primitive or array is allocated via sharedctypes,
the runtime:
- Requests a block of shared memory from the operating system sized to
fit the specified
ctypesdata type. - Constructs a
ctypesobject directly over this raw memory address. - Passes the memory reference or relies on process inheritance (such
as
forkon POSIX systems or handle duplication on Windows) so child processes map the identical physical memory pages into their own virtual address spaces.
Because the memory addresses correspond to the same physical RAM, any write operation performed by one process is instantly visible to all other processes attached to that memory block.
C-Compatible Types and the ctypes Layer
The module integrates with Python's built-in ctypes
library to enforce strict memory layouts. Instead of high-level,
dynamically typed Python objects—which contain interpreter-specific
metadata, reference counts, and pointers that cannot safely span
different virtual address spaces—sharedctypes operates
strictly on flat, contiguous C primitives.
Supported structures include:
- Standard primitive types (e.g.,
ctypes.c_int,ctypes.c_double,ctypes.c_char) - Structured types defined via
ctypes.Structureandctypes.Union - Fixed-size contiguous arrays of primitives
By utilizing flat data structures, processes avoid passing pointers that reference private virtual memory addresses, eliminating memory corruption errors.
Raw Objects vs. Synchronized Wrappers
Concurrency requires synchronization. Modifying raw memory
concurrently across processes leads to race conditions. To address this,
multiprocessing.sharedctypes provides two main interfaces:
raw objects and synchronized wrappers.
Raw Objects
Constructors such as RawValue and RawArray
allocate ctypes instances in shared memory without any locking
mechanisms:
from multiprocessing.sharedctypes import RawValue, RawArray
import ctypes
# Allocates an unmanaged shared double and an array of 10 integers
shared_num = RawValue(ctypes.c_double, 3.14)
shared_arr = RawArray(ctypes.c_int, [0] * 10)These functions offer maximum performance because read and write
operations incur zero lock overhead. However, the programmer is entirely
responsible for coordinating access via external synchronization
primitives (such as multiprocessing.Lock or
multiprocessing.Semaphore).
Synchronized Objects
The standard Value and Array factory
functions automatically wrap the raw ctypes objects inside
synchronization proxies (Synchronized and
SynchronizedArray):
from multiprocessing.sharedctypes import Value, Array
import ctypes
# Allocates an integer and array protected by recursive locks
sync_int = Value(ctypes.c_int, 42, lock=True)
sync_arr = Array(ctypes.c_char, b"hello", lock=True)By default, these wrappers instantiate a
multiprocessing.RLock alongside the shared buffer. When
interacting with the wrapper, synchronization is handled in two
ways:
- Implicit Synchronization on Attribute Access:
Reading or assigning to the
.valueattribute automatically acquires and releases the underlying lock. - Explicit Context Management: The wrapper object
itself implements the context management protocol
(
__enter__and__exit__), allowing atomic blocks:
with sync_int.get_lock():
sync_int.value += 1Cross-Process Inheritance and Serialization
When a process spawns a child worker, the shared ctypes reference must be passed down.
- Fork (POSIX): The child process naturally inherits the virtual memory mappings of the parent. The pointer remains valid, meaning no explicit re-mapping is required.
- Spawn (Windows, macOS default): The child process
starts as a fresh interpreter. The runtime serializes a memory reduction
object that contains the underlying shared memory handle and size. When
received by the child, the runtime re-maps the shared memory block into
the child's address space and re-instantiates the
ctypesobject over that memory.
Because the underlying shared storage is managed directly by OS pages, this mechanism completely bypasses Python's Global Interpreter Lock (GIL) and avoids pickling entire datasets, making it an efficient solution for inter-process communication involving numeric computation.