Python Threads
Python Threads
Año 2024
AP Santiago Nicolau
1
PROGRAMACIÓN
Python - Threads
2
Paralelismo y concurrencia son conceptos abstractos que hacen referencia a
la " programación en paralelo " y a la administración de la ejecución de
múltiples procesos.
Paralelismo: conjunto de técnicas empleadas para procesar información de
forma simultánea, con el objetivo de reducir la cantidad de tiempo que
consume la ejecución de un programa.
Concurrencia: forma de gestionar y coordinar la ejecución de múltiples
procesos que se ejecutan de forma intercalada, no necesariamente al mismo
tiempo. Por ejemplo, un servidor web que atiende a cada usuario en un corto
periodo de tiempo.
3
Concurrencias vs Paralelismo
5
Proceso
Un proceso tiene acceso a su propia memoria y está aislado de otros procesos
en ejecución.
En un momento dado, solo un proceso puede estar ejecutándose en una CPU.
¿Cómo es posible que la mayoría de las computadoras manejen múltiples
tareas simultáneamente?.
Este truco se conoce como cambio de contexto: implica que el sistema
operativo cambie rápidamente entre los procesos que se están ejecutando
actualmente tan rápido que los humanos percibimos que está sucediendo al
mismo tiempo.
Un proceso puede ser "single-threaded" o "multithreaded".
6
Procesos - Threads - Sistema Operativo
7
Thread
Thread o "hilo de ejecución": secuencia más pequeña de instrucciones
programadas que puede gestionar el "scheduler" del sistema operativo.
Los threads forman parte de los procesos: se crean, ejecutan y finalizan
dentro de un proceso.
Un proceso puede tener múltiples threads.
8
Thread
Cada programa Python es un proceso con un hilo llamado hilo principal que
se utiliza para ejecutar las instrucciones del programa.
Cada proceso es una instancia del intérprete de Python que ejecuta las
instrucciones de Python.
9
Thread
El siguiente programa al ejecutarse se convierte en un proceso monotarea
(que solo tiene un thread, el programa en si).
def alCuadrado():
for i in range(1, 101):
cuadrado = i ** 2
print(f"El cuadrado de {i} es: {cuadrado}")
if __name__ == "__main__":
alCuadrado()
10
Thread
import threading # Importar el módulo
def alCuadrado():
for i in range(1, 101):
cuadrado = i ** 2
print(f"El cuadrado de {i} es: {cuadrado}")
def alCubo():
for i in range(1, 101):
cubo = i ** 3
print(f"El cubo de {i} es: {cubo}")
if __name__ == "__main__":
# Creación de/los thread(s)
t_cuad = threading.Thread(target=alCuadrado)
t_cubo = threading.Thread(target=alCubo)
# Ejecución de/los thread(s)
t_cuad.start()
t_cubo.start()
# Esperar hasta que se finalizen ambos hilos
t_cuad.join()
t_cubo.join() 11
Thread
Un subproceso o thread realiza la tarea del cuadrado y el otro la del cubo.
Como se mencionó anteriormente, solo se puede ejecutar un proceso en la
CPU. Incluso si el proceso es multi-threaded, solo se puede ejecutar uno de
sus subprocesos a la vez, y el sistema operativo selecciona cuál.
Las computadoras modernas tienen varios núcleos y cada núcleo puede
ejecutar su propio proceso individual.
Una CPU con dos núcleos puede manejar dos tareas simultáneamente .En
teoría, esto significa que nuestro programa multi-threaded, puede ejecutarse
en dos núcleos separados simultáneamente. Esto se llama paralelismo y es
uno de los beneficios que vienen con el multiproceso.
12
Nuevo Thread
start()
Thread Ejecutable
run()
Thread en ejecución
13
Thread - Atributos
Nombre: Dentro de cada proceso los Threads se nombran automáticamente
con el formato “Thread-%d”, donde %d es un entero que indica el número de
Thread (dentro del proceso).
t = threading.Thread(target=funcionTarea)
print("t.name:",t.name)
14
Thread - Ejemplo
import time
import threading
def task(nombre):
print("Iniciando Cuenta Regresiva desde ",nombre)
n=10
while n>0:
time.sleep(1) # bloqueo de 1 segundo
print(nombre,'>', n)
n-=1
if __name__ == '__main__':
t = threading.Thread(target=task, args=('A'))
print("t.name:",t.name,t.ident)
t.start()
print("t ID: ",t.ident, ' n id: ',t.native_id)
print('Esperando finalización de thread ...')
t.join() 15
Thread - Atributos
Daemon: thread que se ejecutan en segundo plano se denominan thread
daemon. De manera predeterminada, los threads no son thread de demonio.
Elnative_id atributo daemon devuelve True o False .
Identificador nativo: Cada thread que creamos es en realidad creado,
administrado por el sistema operativo y se le da un identificador único nativo.
El atributo native_id se asigna después del start() .
Alive: comprobar si un thread está activo mediante el método is_alive()
que devuelve True o False .
16
Thread - Utilidades
Número de subprocesos activos
threading.active_count() : devuelve un entero que indica el número de
threads que están “vivos”.
Subproceso actual
threading.current_thread() : devuelve una instancia de threading.Thread.
Identificador de thread
threading.get_ident() : se obtiene el identificador de subproceso de Python
para el thread actual.
17
Thread - Utilidades
Identificador nativo de thread
threading.get_native_id() : se obtiene el identificador nativo asignado por el
sistema operativo.
Enumerar threads activos
threading.enumerate() : se obtiene una lista de todos los threads activos
dentro de un proceso de Python. Solo se incluirán en la lista los que estén
“vivos”, es decir, aquellos que estén ejecutando actualmente su función run().
La l ista siempre incluirá el thread principal.
18
Thread - GIL
Los Threads en Python no pueden ejecutarse en paralelo debido a GIL
(Global Interpreter Lock).
Python tiene GIL porque no fue diseñado para ser seguro para subprocesos
(thread safe). El multihilo es una característica muy compleja.
GIL le dice a cualquier proceso Python en ejecución que solo uno de sus
subprocesos puede ejecutarse a la vez, incluso si está en una CPU de
múltiples núcleos.
19
Thread - GIL
El porque del GIL, considerar el siguiente ejemplo: se tiene dos subprocesos,
t1 y t2, cada uno ejecutándose en núcleos separados. Ambos tienen acceso a
una lista, l1 = [1, 2, 3].
Si t1 toma la lista y la vacía l1 = [], entonces t2 intenta acceder al primer
índice de una lista vacía, pensando que todavía tiene valores dentro de ella,
eso será un problema. Este es un ejemplo de una condición de carrera.
En Python las listas y otros objetos no fueron diseñados para usarse en un
entorno multiproceso (eso es lo que significa no ser seguro para
subprocesos). Es por eso que el equipo de Python decidió bloquear todo para
que no ocurran errores como estos.
20
import threading
import os
# Los threads se ejecutan en paralelo.
# El código proporciona información sobre el ID del proceso y los nombres de los threads.
def tarea1():
print("> Tarea 1 asignada al thread: {}".format(threading.current_thread().name))
print("> ID del proceso que ejecuta la Tarea 1: {}\r\n".format(os.getpid()))
def tarea2():
print("# Tarea 2 asignada al thread: {}\r\n".format(threading.current_thread().name))
print("# ID del proceso que ejecuta la Tarea 2: {}\r\n".format(os.getpid()))
if __name__ == "__main__":
print("-"*80)
print("ID del proceso que ejecuta el programa: {}".format(os.getpid()))
print("Nombre del Thread Main: {}".format(threading.current_thread().name))
print("-"*80)
t1 = threading.Thread(target=tarea1, name='t1')
t2 = threading.Thread(target=tarea2, name='t2')
t1.start()
t2.start()
t1.join()
t2.join()
21
Como alguna vez lo mencionó el tío de Peter Parker: Un gran poder conlleva
una gran responsabilidad y con los threads no es la excepción.
Python proporciona la biblioteca " multiprocessing " que si permite el
paralelismo.
22
Problemas
Condición de carrera (race condition): cuando múltiples procesos o threads
intentan simultáneamente modificar y recuperar datos compartidos,
generando resultados imprevistos y no intencionales. Esto ocurre cuando la
sincronización de los threads no están coordinadas.
Bloqueos (Deadlocks): cuando 2 o más threads están esperandose
mututamente que terminen o continuen pero de ahi no salen.
Uso excesivo de recursos: la creación de demasiados threads puede generar
un uso excesivo de recursos, generando problemas de rendimiento.
23
Problemas
Rendimiento lento: los programas simultáneos a veces pueden ejecutarse
más lentamente que los programas secuenciales debido a la sobrecarga que
implica la administración de múltiples threads.
Dificultad en la depuración: la depuración puede ser un desafío, ya que el
comportamiento de los threads puede ser impredecible.
Limitaciones del GIL: puede limitar el rendimiento de los programas.
24
ThreadPool
Grupo de threads que se crean de antemano y luego se pueden reutilizar para ejecutar
varias tareas. El módulo concurrent.futures proporciona la clase ThreadPoolExecutor que
facilita la creación y la gestión de un grupo de threads.
Pueden ayudar a mejorar el rendimiento y reducir la sobrecarga al limitar la cantidad de
subprocesos creados y administrar su ciclo de vida de manera más eficiente.
En lugar de crear un nuevo subproceso para cada tarea, un grupo de subprocesos puede
reutilizar subprocesos existentes, lo que puede reducir la sobrecarga de la creación y
destrucción de subprocesos.
Es ideal para crear bucles de tareas vinculadas a E/S simultáneas y para ejecutar tareas
de forma asincrónica.
25