Skip to the solution
HardDSA17 min read

How does multiprocessing share data between processes?

691 views

The problem

Question presented to candidate: "Threads in one Python process can all see the same list. Separate processes cannot. So when you use multiprocessing, how does data actually get from one process to another โ€” and when would you pick a queue, a shared Value, shared_memory or a Manager?"

What a strong answer should cover:

  • Each process has its own memory (address space), so by default nothing is shared: arguments, queue items and return values are pickled, sent over a pipe, and unpickled as a copy on the other side. Mutating the copy never changes the original, and unpicklable objects (lambdas, locks, generators, open sockets) cannot be sent at all.
  • Message passing with Queue (many producers/consumers, a feeder thread plus a pipe underneath) and Pipe (two connected ends) is the default, safest design.
  • Real shared memory: Value / Array (ctypes objects in shared memory, with a lock for read-modify-write) and multiprocessing.shared_memory.SharedMemory (raw named bytes, zero-copy, added in 3.8, with close() per process and exactly one unlink()).
  • Manager objects: a server process owns the real dict/list and others call it through proxies โ€” flexible but every operation is a round trip, and mutating a nested object through a proxy is silently lost.
  • Start methods matter: fork copies the parent (copy-on-write), spawn starts a fresh interpreter and re-imports the main module (hence if __name__ == "__main__":), forkserver forks from a clean server; Linux defaults to fork up to 3.13 and to forkserver from 3.14, macOS and Windows use spawn.

Clarifying questions expected:

  • How big is the data, and is it read-only or updated by many workers?
  • Which OS and Python version (it decides the default start method)?

Code / implementation expected: Optional โ€” a worker fed by a Queue, or a counter in a Value guarded by its lock.

  • multiprocessing
  • ipc

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

17 min read ยท 15 sections

Run the code

Edit the code and run it here. Each console line with an expected value becomes a test.

PythonWhat crosses a process boundary: pickled copies, unpicklable objects, raw bytes
PythonQueue, Pipe, Value and Array across real processes
# Message passing (Queue, Pipe) and shared ctypes (Value, Array).
# Needs real processes, so it runs on CPython, not in the browser.
import multiprocessing as mp


def square_worker(inbox, outbox):
    # Each item arrives as a pickled copy; the result goes back the same way.
    for n in iter(inbox.get, None):          # None is the stop sentinel
        outbox.put((n, n * n))


def pipe_worker(conn):
    msg = conn.recv()
    conn.send({"echo": msg, "len": len(msg)})
    conn.close()


def add_locked(counter, times):
    for _ in range(times):
        with counter.get_lock():             # Value carries its own lock
            counter.value += 1


def fill(arr, start, step):
    for i in range(start, len(arr), step):
        arr[i] = i * 10                      # writes land in shared memory


if __name__ == "__main__":
    ctx = mp.get_context("spawn")            # same behaviour on every OS
    print("start method:", ctx.get_start_method())

    # 1. Queue: many items, one direction per queue
    inbox, outbox = ctx.Queue(), ctx.Queue()
    p = ctx.Process(target=square_worker, args=(inbox, outbox))
    p.start()
    for n in range(5):
        inbox.put(n)
    inbox.put(None)
    results = sorted(outbox.get() for _ in range(5))
    p.join()
    print("queue results:", results)

    # 2. Pipe: two connected ends, request/response
    parent_end, child_end = ctx.Pipe()
    p = ctx.Process(target=pipe_worker, args=(child_end,))
    p.start()
    parent_end.send("hello")
    print("pipe reply:", parent_end.recv())
    p.join()

    # 3. Value: one shared C int, 4 processes, guarded by its lock
    counter = ctx.Value("i", 0)
    workers = [ctx.Process(target=add_locked, args=(counter, 5_000)) for _ in range(4)]
    for w in workers:
        w.start()
    for w in workers:
        w.join()
    print("locked counter:", counter.value)

    # 4. Array: shared C doubles, two processes fill alternate slots
    arr = ctx.Array("d", 6)
    workers = [ctx.Process(target=fill, args=(arr, k, 2)) for k in (0, 1)]
    for w in workers:
        w.start()
    for w in workers:
        w.join()
    print("shared array:", list(arr))
    print("exit codes:", [w.exitcode for w in workers])
PythonSharedMemory, ShareableList and a Manager proxy
# SharedMemory (zero-copy bytes), ShareableList, and a Manager proxy.
import multiprocessing as mp
import struct
from multiprocessing import shared_memory


def scale_in_place(name, count, factor):
    # Attach to the existing block BY NAME: no data is copied.
    shm = shared_memory.SharedMemory(name=name)
    try:
        for i in range(count):
            (x,) = struct.unpack_from("<d", shm.buf, i * 8)
            struct.pack_into("<d", shm.buf, i * 8, x * factor)
    finally:
        shm.close()                          # detach, do NOT unlink here


def bump_list(name):
    sl = shared_memory.ShareableList(name=name)
    sl[0] = sl[0] + 1
    sl[2] = "done"                           # must fit the original slot size
    sl.shm.close()


def use_manager(d, log):
    d["worker"] = "was here"                 # proxy call -> manager process
    log.append("appended via proxy")
    d["nested"]["x"] = 99                    # mutates a COPY, then lost
    inner = d["nested"]
    inner["y"] = 1
    d["nested"] = inner                      # reassign so the manager stores it


if __name__ == "__main__":
    ctx = mp.get_context("spawn")

    # 1. Raw shared memory: 4 doubles = 32 bytes
    values = [1.5, 2.0, 3.25, 4.0]
    shm = shared_memory.SharedMemory(create=True, size=8 * len(values))
    try:
        for i, v in enumerate(values):
            struct.pack_into("<d", shm.buf, i * 8, v)
        p = ctx.Process(target=scale_in_place, args=(shm.name, len(values), 10))
        p.start(); p.join()
        print("after worker:", [struct.unpack_from("<d", shm.buf, i * 8)[0] for i in range(4)])
        print("block size >= 32:", shm.size >= 32)
    finally:
        shm.close()
        shm.unlink()                         # the creator frees it exactly once

    # 2. ShareableList: fixed-length list of int/float/str/bytes/bool/None
    sl = shared_memory.ShareableList([41, 3.5, "pending"])
    try:
        p = ctx.Process(target=bump_list, args=(sl.shm.name,))
        p.start(); p.join()
        print("shareable list:", list(sl))
    finally:
        sl.shm.close()
        sl.shm.unlink()

    # 3. Manager: a server process owns real objects, others hold proxies
    with ctx.Manager() as manager:
        d = manager.dict({"nested": {}})
        log = manager.list()
        p = ctx.Process(target=use_manager, args=(d, log))
        p.start(); p.join()
        print("manager dict:", dict(sorted(d.items())))
        print("manager list:", list(log))
        print("proxy type:", type(d).__name__)

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 127 of 139 in Python

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Python questions
Up nextHard

What is the Method Resolution Order (MRO)?

Continue