Hilos de Python – Sección Crítica

¿Qué es la “sección crítica” de un hilo (en Python)?

Un subproceso entra en la sección crítica llamando al método adquirir (), que puede estar bloqueando o no bloqueando. Un hilo sale de la sección crítica, llamando al método release ().

– Entendiendo el subprocesamiento en Python, Linux Gazette

Además, ¿cuál es el propósito de una cerradura?

Una sección crítica de código es aquella que solo puede ser ejecutada por un hilo a la vez. Tome un servidor de chat, por ejemplo. Si tiene un hilo para cada conexión (es decir, cada usuario final), una “sección crítica” es el código de spooling (enviar un mensaje entrante a todos los clientes). Si más de un hilo trata de poner en cola un mensaje a la vez, obtendrás BfrIToS mANtwD PIoEmesCEsaSges entrelazados, lo que obviamente no es bueno en absoluto.

Un locking es algo que se puede usar para sincronizar el acceso a una sección crítica (o recursos en general). En nuestro ejemplo de servidor de chat, el locking es como una habitación cerrada con una máquina de escribir. Si hay un hilo dentro (para escribir un mensaje), ningún otro hilo puede entrar en la sala. Una vez hecho el primer hilo, abre la habitación y se va. Luego, otro hilo puede entrar en la habitación (bloquearlo). “Buscar” la cerradura solo significa “Tengo la habitación”.

Otras personas han dado muy buenas definiciones. Aquí está el ejemplo clásico:

import threading account_balance = 0 # The "resource" that zenazn mentions. account_balance_lock = threading.Lock() def change_account_balance(delta): global account_balance with account_balance_lock: # Critical section is within this block. account_balance += delta 

Digamos que el operador += consiste en tres subcomponentes:

  • Lee el valor actual
  • Agregue el RHS a ese valor
  • Escriba el valor acumulado de nuevo en el LHS (técnicamente encuéntrelo en términos de Python)

Si no tiene la statement with account_balance_lock y ejecuta dos llamadas change_account_balance en paralelo, puede terminar entrelazando las tres operaciones del subcomponente de una manera peligrosa. Digamos que simultáneamente llama change_account_balance(100) (pos AKA) y change_account_balance(-100) (AKA neg). Esto podría suceder:

 pos = threading.Thread(target=change_account_balance, args=[100]) neg = threading.Thread(target=change_account_balance, args=[-100]) pos.start(), neg.start() 
  • pos: leer el valor actual -> 0
  • neg: leer el valor actual -> 0
  • pos: agregar el valor actual para leer el valor -> 100
  • neg: agrega el valor actual para leer el valor -> -100
  • pos: escriba el valor actual -> account_balance = 100
  • neg: escriba el valor actual -> account_balance = -100

Debido a que no obligó a las operaciones a realizarse en segmentos discretos, puede tener tres resultados posibles (-100, 0, 100).

La instrucción with [lock] es una operación única e indivisible que dice: “Permítame ser el único hilo que ejecuta este bloque de código. Si se está ejecutando algo más, está bien, esperaré”. Esto garantiza que las actualizaciones de account_balance sean “seguras para subprocesos” (seguras para el paralelismo).

Nota: Hay una advertencia en este esquema: debe recordar adquirir la account_balance_lock (vía with ) cada vez que quiera manipular la account_balance para que el código permanezca seguro para subprocesos. Hay maneras de hacer que esto sea menos frágil, pero esa es la respuesta a otra pregunta.

Edición: En retrospectiva, es probable que sea importante mencionar que la instrucción with llama implícitamente una acquire locking en el locking. Esta es la parte “Voy a esperar” del diálogo del hilo anterior. En contraste, una adquisición no bloqueante dice: “Si no puedo adquirir el locking de inmediato, avíseme”, y luego confía en usted para verificar si obtuvo el locking o no.

 import logging # This module is thread safe. import threading LOCK = threading.Lock() def run(): if LOCK.acquire(False): # Non-blocking -- return whether we got it logging.info('Got the lock!') LOCK.release() else: logging.info("Couldn't get the lock. Maybe next time") logging.basicConfig(level=logging.INFO) threads = [threading.Thread(target=run) for i in range(100)] for thread in threads: thread.start() 

También quiero agregar que el propósito principal del locking es garantizar la atomicidad de la adquisición (la indivisibilidad de la acquire través de los hilos), que una simple bandera booleana no garantiza. La semántica de las operaciones atómicas probablemente sea también el contenido de otra pregunta.

Una “sección crítica” es un fragmento de código en el que, para ser correctos, es necesario asegurarse de que solo un hilo de control pueda estar en esa sección a la vez. En general, necesita una sección crítica para contener referencias que escriban valores en la memoria que puedan compartirse entre más de un proceso concurrente.