6 - Memoria Compartida (Shared Memory)

Compartir grandes volúmenes de datos entre procesos vía colas o pipes puede ser lento por la serialización (pickle) y la copia. La memoria compartida permite que varios procesos accedan al mismo buffer sin copiarlo, ahorrando tiempo y RAM.
Ganas velocidad, pero también asumes la responsabilidad de sincronizar accesos y liberar el segmento.

6.1 La necesidad de compartir memoria entre procesos

  • Evitar copias grandes: pasar matrices o arrays por cola multiplica el consumo y el tiempo de IPC.
    Cada salto por la cola duplica memoria y tiempo de serialización.
  • IPC vs shared memory: pipes/queues son seguros pero copian datos; shared memory expone un segmento común y requiere sincronización manual.
    La elección es entre facilidad (colas) y eficiencia (memoria compartida).

6.2 shared_memory en multiprocessing (Python 3.8+)

  • SharedMemory: crea un bloque con nombre (name) o se adjunta a uno existente.
    El nombre permite que otros procesos lo encuentren.
  • Crear, adjuntar, cerrar, liberar: SharedMemory(create=True, size=...) para crear; SharedMemory(name=...) para adjuntar; close() cierra el handle; unlink() libera el segmento.
    Olvidar unlink() deja el segmento huérfano.
  • Buffers compartidos: se manipulan con memoryview o se convierten en arrays de numpy o array para acceso cómodo.
    No hay copia: ambos procesos ven el mismo bloque.

6.3 Arreglos y estructuras compartidas

  • multiprocessing.Array: crea un buffer compartido tipado (por ejemplo 'i' para enteros, 'd' para doubles).
    Sirve para estructuras fijas y sencillas.
  • multiprocessing.Value: comparte un solo valor primitivo.
    Útil para contadores o flags globales.
  • Sincronización: ambos pueden recibir un lock (por defecto True) para proteger escrituras.
    Desactívalo solo si manejas rangos sin solapamiento.

6.4 Riesgos de la memoria compartida

  • Inconsistencias: escribir sin coordinación puede dejar datos corruptos.
    Planifica qué proceso toca cada región.
  • Race conditions: dos procesos pueden pisarse; usa multiprocessing.Lock o división estricta de rangos.
    Prefiere particionar rangos para evitar bloquear constantemente.
  • Complejidad extra: debes cerrar y liberar (unlink) el segmento; si lo olvidas, quedan recursos ocupados.
    Incluye siempre close() y unlink() en un bloque de limpieza.

6.5 Aplicación: Procesamiento paralelo con shared_memory

Este programa crea un arreglo grande en memoria compartida, reparte su procesamiento entre procesos y combina el resultado sin copiar datos.
La clave es dividir el buffer en tramos disjuntos para que cada proceso trabaje sin bloquear.

import math
import multiprocessing as mp
from multiprocessing import shared_memory
import time


def inicializar(shm_name, length):
    shm = shared_memory.SharedMemory(name=shm_name)
    buf = memoryview(shm.buf).cast("I")  # unsigned int
    for i in range(length):
        buf[i] = i
    buf.release()
    shm.close()


def procesar_parte(shm_name, start, end):
    shm = shared_memory.SharedMemory(name=shm_name)
    buf = memoryview(shm.buf).cast("I")
    for i in range(start, end):
        buf[i] = buf[i] * 2 + (1 if buf[i] % 2 == 0 else 0)
    buf.release()
    shm.close()


def verificar(shm_name, length):
    shm = shared_memory.SharedMemory(name=shm_name)
    buf = memoryview(shm.buf).cast("I")
    ok = all(buf[i] == i * 2 + (1 if i % 2 == 0 else 0) for i in range(length))
    buf.release()
    shm.close()
    return ok


def main():
    length = 500_000
    size_bytes = length * 4  # uint32
    shm = shared_memory.SharedMemory(create=True, size=size_bytes)
    shm_name = shm.name

    # Inicializa datos en el segmento
    inicializar(shm_name, length)

    workers = mp.cpu_count()
    chunk = math.ceil(length / workers)
    procesos = []
    inicio = time.perf_counter()
    for idx in range(workers):
        start = idx * chunk
        end = min(start + chunk, length)
        if start >= end:
            continue
        p = mp.Process(target=procesar_parte, args=(shm_name, start, end))
        p.start()
        procesos.append(p)

    for p in procesos:
        p.join()
    duracion = time.perf_counter() - inicio

    ok = verificar(shm_name, length)
    print(f"Procesado en {duracion:.2f}s con {len(procesos)} procesos. Verificación: {ok}")

    shm.close()
    shm.unlink()  # libera el segmento


if __name__ == "__main__":
    mp.freeze_support()  # seguro en Windows
    main()

El ejemplo evita copiar datos grandes: todos los procesos leen y escriben sobre el mismo buffer. Aun así, es necesario dividir rangos para no pisar memoria y limpiar con close() y unlink() para liberar el segmento compartido.