Python soluciona problemas de memoria compartida y bloquea recursos compartidos

Salman Mehmood 21 junio 2023
  1. Use multiprocessing.Array() para usar la memoria compartida en Python
  2. Use multiprocessing.Lock() para bloquear los recursos compartidos en Python
Python soluciona problemas de memoria compartida y bloquea recursos compartidos

Este tutorial explica diferentes aspectos del multiprocesamiento de la memoria compartida y demuestra cómo solucionar problemas con la memoria compartida. También aprenderemos a usar el bloqueo para bloquear los recursos compartidos en Python.

Use multiprocessing.Array() para usar la memoria compartida en Python

Uno de los aspectos más críticos del multiprocesamiento es compartir los datos entre los procesos cuando tiene múltiples procesos secundarios.

Una de las propiedades esenciales de los procesos secundarios que crea utilizando la capacidad del módulo de procesamiento es que se ejecutan de forma independiente y tienen su propio espacio de memoria.

Significa que el proceso del niño tendrá algún espacio de memoria. Y, cualquier variable intenta crear o será cambiada en su propio espacio de memoria, no en el espacio de memoria de su padre.

Intentemos entender este concepto tomando un ejemplo y saltando al código importando el módulo de multiprocesamiento.

Creamos una lista vacía llamada RESULTADO, y hemos definido una función llamada Make_Sqaured_List(), que cuadra los elementos de una lista dada y los agrega a nuestra lista global de RESULTADOS.

El objeto Procc_1 es igual a la clase Process() y establece el destino como la función Make_Sqaured_List sin paréntesis.

Y, al parámetro args, le pasamos una lista separada llamada Items_list que se dará como argumento a la función Make_Sqaured_List().

Código de ejemplo:

import multiprocessing

RESULT = []


def Make_Sqaured_List(Num_List):
    global RESULT

    for n in Num_List:
        RESULT.append(n ** 2)
    print(f"Result: {RESULT}")


if __name__ == "__main__":
    Items_list = [5, 6, 7, 8]

    Procc_1 = multiprocessing.Process(target=Make_Sqaured_List, args=(Items_list,))
    Procc_1.start()
    Procc_1.join()

Ejecutemos este proceso secundario y obtendremos el resultado de acuerdo con nuestro proceso secundario que es el valor de la lista global.

Result: [25, 36, 49, 64]

Pero, si intenta imprimir la lista de RESULTADOS que aún está vacía, ¿qué está pasando con la lista de RESULTADOS?

print(RESULT)

Producción :

[]

De acuerdo con nuestro proceso principal, nuestro proceso principal todavía está vacío, mientras que de acuerdo con el proceso secundario, la lista RESULTADO tiene algo de contenido. Simplemente significa que nuestros diferentes procesos tienen diferentes espacios de memoria.

Podemos entender esto por un escenario en el que tenemos el proceso uno, que es nuestro programa principal donde inicialmente tenemos una lista de RESULTADOS vacía.

Y, cuando creamos un proceso secundario, también tiene un vacío inicialmente, y luego se ejecuta la función Make_Sqaured_List(), por lo que la lista RESULTADO contiene algunos elementos. Pero, dado que estamos accediendo a RESULTADO desde el proceso principal en este espacio de memoria, los cambios no son visibles.

Código de ejemplo:

import multiprocessing

RESULT = []


def Make_Sqaured_List(Num_List):
    global RESULT

    for n in Num_List:
        RESULT.append(n ** 2)
    print(f"Result: {RESULT}")


if __name__ == "__main__":
    Items_list = [5, 6, 7, 8]

    Procc_1 = multiprocessing.Process(target=Make_Sqaured_List, args=(Items_list,))
    Procc_1.start()
    Procc_1.join()
    print(RESULT)

Producción :

Result: [25, 36, 49, 64]
[]

Entonces, ¿cuál es la solución para esto? Pero, primero, veamos cómo podemos solucionarlo.

Solución para solucionar los problemas con el intercambio de datos entre multiprocesamiento

En la sección, veremos la solución que nos ayuda a obtener el valor de cualquier cambio y solucionar el problema de compartir datos entre el multiprocesamiento.

La solución se llama memoria compartida. El módulo de multiprocesamiento nos proporciona dos tipos de objetos llamados Array y Value que se pueden utilizar para compartir los datos entre los procesos.

El Array es un array asignado desde la memoria compartida; básicamente, hay una porción en la memoria de su computadora que podemos llamar memoria compartida o una región a la que pueden acceder múltiples procesos.

Entonces, en esa memoria compartida, creamos una nueva matriz o un nuevo valor. Estos valores agregados no son nuestras estructuras de datos básicas de Python; hay algo diferente y definido en el propio módulo de multiprocesamiento.

Ahora declaramos un objeto llamado RESULT_ARRAY usando multiprocessing.Array(). Luego, en esta matriz, tenemos que pasar el tipo de datos. Pasamos i como una cadena, lo que significa que pondremos valores enteros dentro, y tenemos que dar el tamaño.

Código de ejemplo:

RESULT_ARRAY = multiprocessing.Array("i", 4)

Está relacionado con una matriz de estilo de programación C para que podamos dar el tamaño simultáneamente. De esta manera, podemos almacenar objetos en el lugar deseado.

Ahora estamos creando un nuevo valor llamado OBJ_Sum que es igual a multiprocessing.Value(), y almacenará e ingresará el valor.

Código de ejemplo:

OBJ_Sum = multiprocessing.Value("i")

A continuación, crearemos un objeto llamado procc_1, que será igual a multiprocesamiento.Proceso(), al que llamaremos función. Creamos una función llamada Make_Sqaured_List(), que tomará tres argumentos:

  1. Una lista
  2. Un objeto de matriz
  3. Un objeto de valor.

Pasaremos estos tres argumentos a nuestra función usando este argumento Proceso llamado args. Por ejemplo, mire la siguiente valla de código.

Código de ejemplo:

Procc_1 = multiprocessing.Process(
    target=Make_Sqaured_List, args=(Items_list, RESULT_ARRAY, OBJ_Sum)
)

Ahora en la función Make_Sqaured_List(), vamos a iterar Items_list usando la función enumerate(). Para que podamos obtener el índice y el valor de Items_list.

Es una matriz de estilo C, por lo que tendremos que usar la indexación para asignar los valores a nuestra matriz. También sumaremos los valores de una matriz, y OBJ_Sum.value es una propiedad de multiprocessing.Value().

Código de ejemplo:

def Make_Sqaured_List(Items_list, RESULT, OBJ_Sum):

    for i, n in enumerate(Items_list):
        RESULT[i] = n ** 2

    OBJ_Sum.value = sum(RESULT)

Hemos definido algunas variables en nuestro proceso principal, cambiando la función llamada por el proceso hijo. Entonces, nuestra agenda principal es si podemos obtener esos valores modificados en nuestro proceso principal o no.

Ahora podemos acceder a la matriz reflejada en el proceso hijo y obtener su suma usando OBJ_Sum.value. Por ejemplo, consulte el siguiente fragmento de código.

Código de ejemplo:

import multiprocessing


def Make_Sqaured_List(Items_list, RESULT, OBJ_Sum):

    for i, n in enumerate(Items_list):
        RESULT[i] = n ** 2

    OBJ_Sum.value = sum(RESULT)


if __name__ == "__main__":
    Items_list = [5, 6, 7, 8]

    RESULT_ARRAY = multiprocessing.Array("i", 4)
    OBJ_Sum = multiprocessing.Value("i")

    Procc_1 = multiprocessing.Process(
        target=Make_Sqaured_List, args=(Items_list, RESULT_ARRAY, OBJ_Sum)
    )
    Procc_1.start()
    Procc_1.join()
    print(RESULT_ARRAY[:])
    print(OBJ_Sum.value)

Producción :

[25, 36, 49, 64]
174

De esta manera, podemos realizar cualquier cambio en nuestros objetos definidos en nuestro proceso principal y los cambios que obtenemos del proceso secundario. Esto es posible gracias a la técnica de memoria compartida.

Use multiprocessing.Lock() para bloquear los recursos compartidos en Python

Cubriremos un tema crucial llamado bloqueo; ahora bien, si has tomado clases de informática o de sistema operativo, ya aprendiste sobre el candado. Sin embargo, el bloqueo es un concepto crítico cuando se trata de multiprocesamiento y conceptos de sistema operativo.

Primero, consideremos por qué se necesita el candado en la vida real; en nuestro día a día, algunos recursos no pueden ser accedidos por dos personas simultáneamente.

Por ejemplo, la puerta del baño tiene una cerradura porque si dos personas intentan acceder a ella simultáneamente, se creará una situación bastante embarazosa. Por eso protegemos el baño, un recurso compartido con una cerradura.

De manera similar, en el mundo de la programación, cada vez que dos procesos o subprocesos intentan acceder a un recurso compartido, como un archivo de memoria compartido o una base de datos. Puede crear un problema, por lo que debes proteger ese acceso con un candado.

Que pasa si no añadimos esa protección a nuestro programa, eso lo veremos ejecutando un ejemplo. Nuevamente, este es un programa de software bancario, y aquí tenemos dos procesos.

El primer proceso es depositar dinero en un banco usando la función MONEY_DP() y el segundo es retirar dinero del banco usando la función MONEY_WD(). Y al final, estamos imprimiendo el balance final.

Empezamos con un saldo de 200$ en dólares en la sección MONEY_DP. Estamos depositando 100$, tenemos un ciclo for que rastreamos 100 veces, y en cada iteración, agregará 01$ a nuestra cuenta bancaria.

De manera similar, en la función MONEY_WD, tenemos el mismo ciclo que itera 100 veces y cada vez, deducirá 1 dólares de nuestra cuenta bancaria.

Código de ejemplo:

import multiprocessing
import time


def MONEY_DP(B):
    for i in range(100):
        time.sleep(0.01)
        B.value = B.value + 1


def MONEY_WD(B):
    for i in range(100):
        time.sleep(0.01)
        B.value = B.value - 1

Ahora estamos usando una variable de memoria compartida llamada valor que ya aprendimos en la sección anterior. Este valor de multiprocesamiento es un recurso de memoria compartida, así que veamos qué sucede cuando intentamos ejecutar este programa.

if __name__ == "__main__":
    B = multiprocessing.Value("i", 200)
    Deposit = multiprocessing.Process(target=MONEY_DP, args=(B,))
    Withdrawl = multiprocessing.Process(target=MONEY_WD, args=(B,))
    Deposit.start()
    Withdrawl.start()
    Deposit.join()
    Withdrawl.join()
    print(B.value)

Lo ejecutaremos varias veces, y cada vez, solo está imprimiendo valores diferentes, pero debería imprimir 200$.

Producción :

# execution 1
205
# execution 2
201
# execution 3
193

¿Por qué pasó esto? Ocurre principalmente cuando este proceso intenta leer una variable llamada B.value en la memoria compartida.

Digamos que B.value tiene un valor de 200$, lo leerá y luego agregará uno, y luego volverá a colocar lo mismo en la misma variable.

Dado que B.value es 200$ y está realizando esta operación de adición a nivel del sistema operativo, a nivel del sistema operativo, ejecutará múltiples instrucciones de línea de ensamblaje.

Así hemos leído la variable 200; se suma uno y se vuelve a asignar 201 a la variable B.value.

Código de ejemplo:

B.value = B.value + 1

Ahora, mientras lo hace en ese mismo momento, esta instrucción también se ejecutó en la función MONEY_WD().

Código de ejemplo:

B.value = B.value - 1

Aunque estamos depositando primero y luego retirando, cuando el proceso intente leer B.value, seguirá siendo 200 porque el proceso de depósito no ha vuelto a escribir en la variable original.

En lugar de leer B.value como 201 dentro del proceso MONEY_WD, leerá B.value como 200, y después de disminuir 1, tendrá 199.

Es por eso que estamos obteniendo un comportamiento inconsistente. Primero, usemos Bloquear para bloquear el acceso; ahora, creamos una variable llamada lock y usamos módulos de multiprocesamiento para usar la clase Lock.

Código de ejemplo:

lock = multiprocessing.Lock()

Ahora pasaremos ese lock a ambos procesos y, dentro de ambos procesos, llamaremos a lock.acquire() para poner un lock, y luego para soltar un lock, llamaremos al lock. liberación() función.

Estas funciones de bloqueo protegen la sección de código mientras se accede al recurso compartido, llamado sección crítica.

Código de ejemplo:

import multiprocessing
import time


def MONEY_DP(B, lock):
    for i in range(100):
        time.sleep(0.01)
        lock.acquire()
        B.value = B.value + 1
        lock.release()


def MONEY_WD(B, lock):
    for i in range(100):
        time.sleep(0.01)
        lock.acquire()
        B.value = B.value - 1
        lock.release()


if __name__ == "__main__":
    B = multiprocessing.Value("i", 200)
    lock = multiprocessing.Lock()
    Deposit = multiprocessing.Process(target=MONEY_DP, args=(B, lock))
    Withdrawl = multiprocessing.Process(target=MONEY_WD, args=(B, lock))
    Deposit.start()
    Withdrawl.start()
    Deposit.join()
    Withdrawl.join()
    print(B.value)

Ahora, este código está imprimiendo 200 cada vez.

Producción :

200
PS C:\Users\Dell\Desktop\demo> python -u "c:\Users\Dell\Desktop\demo\demo.py"
200
PS C:\Users\Dell\Desktop\demo> python -u "c:\Users\Dell\Desktop\demo\demo.py"
200
Salman Mehmood avatar Salman Mehmood avatar

Hello! I am Salman Bin Mehmood(Baum), a software developer and I help organizations, address complex problems. My expertise lies within back-end, data science and machine learning. I am a lifelong learner, currently working on metaverse, and enrolled in a course building an AI application with python. I love solving problems and developing bug-free software for people. I write content related to python and hot Technologies.

LinkedIn

Artículo relacionado - Python Error