Problema: ¿Por qué la varible global request en Flask no genera conflictos entre diferentes vistas?
Flask utiliza un objeto global llamado request que contiene información de cada petición HTTP. Sin embargo, al ejecutar múltiples hilos simultáneamente, este objeto global no produce datos cruzados entre peticiones. La clave reside en el uso de almacenamiento local por hilo.
Primero, observemos qué ocurre al compartir una variable global entre múltiples hilos sin ningún mecanismo de aislamiento:
# Ejemplo de problema: variable compartida sin protección
from threading import Thread
import time
contador_global = -1
def procesar(valor):
global contador_global
contador_global = valor
# Simulamos procesamiento asíncrono
time.sleep(2)
print(f"Hilo recibió valor: {contador_global}, pero imprime: {contador_global}")
# Lanzamos 10 hilos concurrentes
for idx in range(10):
worker = Thread(target=procesar, args=(idx,))
worker.start()
Resultado: Todos los hilos imprimirán 9. Esto sucede porque la última escritura sobrescribe las anteriores, y no existe forma de distinguir qué hilo modificó qué valor. Los hilos comparten el mismo espacio de memoria para la variable global.
Solución: Uso de threading.local()
Python proporciona la clase local del módulo threading. Esta clase almacena datos de forma independiente para cada hilo, creando internamente una estructura similar a:
{
id_hilo_1: {'dato': 1},
id_hilo_2: {'dato': 2},
id_hilo_3: {'dato': 3}
}
Cada hilo accede únicamente a su propio diccionario, evitando interferencias:
# Implementación con threading.local()
from threading import Thread, local
import time
contexto = local()
def ejecutar(valor):
contexto.resultado = valor
time.sleep(2)
# Cada hilo obtiene su propio valor almacenado
print(f"Valor recuperado: {contexto.resultado}")
for idx in range(10):
hilo = Thread(target=ejecutar, args=(idx,))
hilo.start()
Implementación personalizada: versión funcional
Para comprender el funcionamiento interno, podemos construir nuestro propio mecanismo de almacenamiento local usando un diccionario indexado por el identificador del hilo:
# Almacenamiento local personalizado usando funciones
from threading import get_ident, Thread
import time
almacen = {}
def guardar(clave, valor):
tid = get_ident()
if tid not in almacen:
almacen[tid] = {}
almacen[tid][clave] = valor
def recuperar(clave):
tid = get_ident()
return almacen[tid][clave]
def tarea(param):
guardar('parametro', param)
resultado = recuperar('parametro')
print(f"Hilo procesó: {resultado}")
for i in range(10):
t = Thread(target=tarea, args=(i,))
t.start()
Implementación orientada a objetos (primera versión)
# Clase Local usando atributo de clase para el almacenamiento
from threading import get_ident, Thread
import time
class ContextoLocal:
_almacen_compartido = {}
def asignar(self, clave, valor):
tid = get_ident()
if tid not in ContextoLocal._almacen_compartido:
ContextoLocal._almacen_compartido[tid] = {}
ContextoLocal._almacen_compartido[tid][clave] = valor
def obtener(self, clave):
tid = get_ident()
return ContextoLocal._almacen_compartido[tid][clave]
instancia = ContextoLocal()
def tarea(param):
instancia.asignar('dato', param)
time.sleep(1)
print(instancia.obtener('dato'))
for i in range(10):
t = Thread(target=tarea, args=(i,))
t.start()
Problema: Al usar un atributo de clase, todas las instancias comparten el mismo diccionario. Si se crean múltiples objetos ContextoLocal, podrían interferir entre sí.
Mejora: Uso de métodos mágicos __setattr__ y __getattr__
# Segunda versión con sobrecarga de operadores
from threading import get_ident, Thread
import time
class ContextoLocalV2:
_datos = {}
def __setattr__(self, clave, valor):
tid = get_ident()
if tid not in ContextoLocalV2._datos:
ContextoLocalV2._datos[tid] = {}
ContextoLocalV2._datos[tid][clave] = valor
def __getattr__(self, clave):
tid = get_ident()
return ContextoLocalV2._datos[tid][clave]
obj = ContextoLocalV2()
def tarea(param):
obj.resultado = param
time.sleep(1)
print(f"Recuperado: {obj.resultado}")
for i in range(10):
t = Thread(target=tarea, args=(i,))
t.start()
Versión corregida: cada instancia con su propio almacenamiento
Para resolver el problema de compartir el diccionario entre instancias, inicializamos el almacenamiento en __init__, invocando directamente el método de la clase padre para evitar recursión infinita:
# Tercera versión con almacenamiento por instancia
from threading import get_ident, Thread
import time
class ContextoLocalV3:
def __init__(self):
# Usamos object.__setattr__ para evitar recursión
object.__setattr__(self, '_almacen', {})
def __setattr__(self, clave, valor):
tid = get_ident()
if tid not in self._almacen:
self._almacen[tid] = {}
self._almacen[tid][clave] = valor
def __getattr__(self, clave):
tid = get_ident()
return self._almacen[tid][clave]
obj = ContextoLocalV3()
def tarea(param):
obj.dato = param
time.sleep(1)
print(obj.dato)
for i in range(10):
t = Thread(target=tarea, args=(i,))
t.start()
Versión final: soporte para hilos y corrutinas (greenlet)
Flask puede trabajar tanto con hilos como con corrutinas (usando greenlet). Para lograr compatibilidad con ambos, detectamos automáticamente el mecanismo de identificación disponible:
# Implementación completa compatible con hilos y corrutinas
try:
from greenlet import getcurrent as obtener_identificador
except ImportError:
from threading import get_ident as obtener_identificador
from threading import Thread
import time
class ContextoLocalCompleto:
def __init__(self):
object.__setattr__(self, '_almacen', {})
def __setattr__(self, clave, valor):
cid = obtener_identificador()
if cid not in self._almacen:
self._almacen[cid] = {}
self._almacen[cid][clave] = valor
def __getattr__(self, clave):
cid = obtener_identificador()
if cid not in self._almacen:
raise AttributeError(f"No existe '{clave}' para el contexto actual")
return self._almacen[cid][clave]
ctx = ContextoLocalCompleto()
def tarea(param):
ctx.parametro = param
ctx.extra = param * 10
print(f"parametro={ctx.parametro}, extra={ctx.extra}")
for i in range(10):
t = Thread(target=tarea, args=(i,))
t.start()
Relación con Flask
Internamente, Flask utiliza una clase similar a werkzeug.local.LocalStack que implementa este mismo patrón. El objeto request no es realmente una variable global simple, sino un proxy que redirige cada acceso al contexto local del hilo actual. De esta forma, cada petición HTTP procesada en un hilo diferente accede a su propio objeto request aislado, evitando cualquier mezcla de datos entre peticiones simultáneas.