Back to Blog
Python

Python Multiprocessing Value and Array: Shared Memory Explained

python multiprocessing value array: Learn how to share data between Python processes using multiprocessing.Value and Array, including synchronization and performance t...

multiprocessingshared memoryprocess synchronizationPython concurrency
Illustration of two Python processes sharing a memory block containing a value and an array, with a lock icon indicating synchronization.

python multiprocessing value array requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you need to share a single value or a fixed-size sequence across processes in Python, multiprocessing.Value and multiprocessing.Array are the direct tools. They allocate memory in a shared region that multiple processes can read and write, unlike ordinary Python objects which are isolated per process. This article explains how to use them correctly, why locking matters, and when they are the right choice compared to other multiprocessing primitives.

Why Regular Variables Don't Work Across Processes

Each Python process has its own memory space. When you fork or spawn a child process, the child receives a copy of the parent's memory at that moment. Any variable you assign in the parent is a separate object in the child. Writing to a plain list or integer in one process does not affect the other. This isolation is fundamental to process safety, but it means you cannot simply share state by passing a reference.

multiprocessing.Value and multiprocessing.Array solve this by placing the underlying data in a shared memory segment. The object you create in the parent is a proxy that points to that segment. When a child process inherits the proxy, it can read and write the same underlying memory, subject to synchronization.

Creating a Shared Value

multiprocessing.Value stores a single value of a specified type. The constructor takes a typecode (a character from the array module) and an initial value. The typecode determines the C type used in shared memory.

from multiprocessing import Process, Value def increment(shared_counter): for _ in range(1000): shared_counter.value += 1 if __name__ == "__main__": counter = Value('i', 0) processes = [Process(target=increment, args=(counter,)) for _ in range(4)] for p in processes: p.start() for p in processes: p.join() print(counter.value)

Here 'i' is the typecode for a signed integer. The .value attribute provides access to the underlying C value. The example increments a shared counter from four processes, but without a lock the final value is likely incorrect because the increment operation is not atomic. The next section shows how to fix that.

Creating a Shared Array

multiprocessing.Array stores a fixed-size sequence of items of a single type. The constructor takes a typecode and either a sequence of initial values or an integer length. When you pass an integer, the array is initialized to zeros.

from multiprocessing import Process, Array def write_values(shared_array): for i in range(len(shared_array)): shared_array[i] = i * 2 if __name__ == "__main__": arr = Array('i', 5) p = Process(target=write_values, args=(arr,)) p.start() p.join() print(arr[:])

The array supports indexing and slicing, and you can convert it to a list with arr[:]. The typecode 'i' again means signed integer. Common typecodes include 'd' for double, 'f' for float, and 'b' for signed char. The shared array is stored in a contiguous memory block, which makes it efficient for numeric data.

Synchronizing Access with Locks

Both Value and Array accept a lock argument. By default, they create a lock that protects every read and write operation. This means operations like value += 1 are performed under the lock, making them safe against race conditions. However, the lock is held only for the duration of the individual operation. If you need to perform multiple operations atomically, you must acquire the lock explicitly.

from multiprocessing import Process, Value, Lock def increment_with_lock(shared_counter, lock): for _ in range(1000): with lock: shared_counter.value += 1 if __name__ == "__main__": counter = Value('i', 0) lock = Lock() processes = [Process(target=increment_with_lock, args=(counter, lock)) for _ in range(4)] for p in processes: p.start() for p in processes: p.join() print(counter.value)

Here we pass a separate Lock and use it as a context manager around the increment. This guarantees that the read-modify-write sequence is atomic. If you pass lock=True to Value or Array, the internal lock is used, but you cannot easily access it for explicit locking. Passing a Lock instance gives you more control.

Performance and Overhead of Shared Memory

Shared memory avoids the serialization and network overhead of pipes or queues. Reading and writing a Value or Array is a direct memory access, so it is fast for small data. However, every operation that touches the shared object acquires a lock. If many processes contend for the same lock, they will block, and the overhead of acquiring and releasing the lock becomes significant.

For large arrays, copying the entire array to a local list for processing can be faster than accessing each element individually under the lock. The lock is per-object, not per-element, so a long loop that updates many elements holds the lock for the entire loop unless you release it between operations. In practice, you should minimize the time spent holding the lock and avoid performing expensive computations while it is held.

Choosing Between Value, Array, and Other Approaches

The decision depends on the shape and lifetime of the data you need to share.

Data shapeRecommended primitiveWhy
Single scalar (int, float, etc.)ValueSimple, low overhead
Fixed-size sequence of numbersArrayContiguous memory, efficient for numeric work
Dynamic or heterogeneous dataManager or QueueMore flexible but slower due to pickling
One-way message passingQueueBuilt-in buffering and ordering
Multiple writers with complex updatesManager with custom objectsHigher-level but slower

Value and Array are the fastest way to share data because they avoid serialization. They are appropriate when the data is small and the access pattern is simple. For larger or more complex structures, multiprocessing.Manager provides a proxy that can hold arbitrary Python objects, but every access goes through a server process and involves pickling, which is significantly slower.

Common Pitfalls and Edge Cases

One frequent mistake is forgetting that the default lock is not reentrant. If a process acquires the internal lock and then attempts to acquire it again on the same object, it will deadlock. This can happen if you call a function that uses the lock while already holding it. Use a separate Lock and manage it explicitly to avoid this.

Another issue is type mismatches. The typecode you choose determines the C type, and assigning a Python value that does not fit can raise an error or silently truncate. For example, assigning a float to a 'i' array raises a TypeError in Python 3. Always verify the typecode matches the data you intend to store.

Finally, shared memory is not persistent. It exists only as long as at least one process holds a reference to the object. If the parent process exits before the children finish, the shared memory may be reclaimed, causing errors in the children. Ensure all processes join before the parent terminates.

When you need to share a fixed-size collection of numeric values across processes, multiprocessing.Array combined with an explicit lock gives you predictable behavior and low overhead. For a single scalar, multiprocessing.Value is the minimal solution. Understanding the locking semantics and the the cost of contention will help you avoid subtle race conditions and performance bottlenecks.

python multiprocessing value array: Practical Usage and Code | RYUSLOG DEV