subprocesos múltiples en Python con ejemplo: Aprenda GIL en Python

⚡ Resumen inteligente

subprocesos múltiples en Python Este método ejecuta varios hilos dentro de un mismo proceso, de modo que comparten memoria y trabajan simultáneamente. El módulo de subprocesos crea y gestiona estos hilos, mientras que el bloqueo del intérprete global limita el paralelismo real, lo que hace que esta técnica sea más adecuada para tareas con alta dependencia de entrada/salida.

  • 🔘 Hilo vs. proceso: Un hilo es una unidad de ejecución dentro de un proceso, y los hilos comparten los datos del proceso mientras se ejecutan individualmente.
  • ☑️ Dos módulos: El módulo obsoleto _thread ofrece llamadas de bajo nivel, mientras que el módulo threading de nivel superior es el estándar moderno para la creación de hilos.
  • Crear hilos: Extiende la clase Thread, sobrescribe __init__ y run(), luego llama a start() para iniciar y a join() para esperar a que finalice.
  • 🧪 Evite las condiciones de carrera: Un bloqueo permite que solo un hilo acceda a una sección crítica a la vez, evitando así interbloqueos y la corrupción de datos compartidos.
  • 🛠️ Entienda el GIL: El bloqueo del intérprete global permite que un hilo se ejecute Python El procesamiento de bytes se realiza de uno en uno, por lo que las tareas que dependen de la CPU requieren multiprocesamiento.
  • 🤖 Cargas de trabajo de IA: El multihilo acelera los pasos de IA que dependen de la entrada/salida, como la carga de datos, mientras que NumPy y PyTorch libera el GIL para computación paralela.

subprocesos múltiples en Python

El Python El lenguaje de programación te permite usar multiprocesamiento o multihilo. En este tutorial, aprenderás cómo escribir aplicaciones multihilo en Python.

¿Qué es un hilo?

Un hilo es una unidad de ejecución en la programación concurrente. La multihilo es una técnica que permite a una CPU ejecutar varias tareas de un mismo proceso simultáneamente. Estos hilos pueden ejecutarse de forma independiente, compartiendo los recursos del proceso.

¿Qué es un proceso?

Un proceso es básicamente el programa en ejecución. Cuando inicias una aplicación en tu ordenador (como un navegador o un editor de texto), el sistema operativo crea un .

¿Qué es el subproceso múltiple en Python?

subprocesos múltiples en Python La programación multihilo es una técnica bien conocida en la que múltiples hilos de un proceso comparten su espacio de datos con el hilo principal, lo que facilita y agiliza el intercambio de información y la comunicación entre hilos. Los hilos son más ligeros que los procesos. Varios hilos pueden ejecutarse individualmente compartiendo sus recursos. El objetivo de la programación multihilo es ejecutar múltiples tareas y funciones simultáneamente.

¿Qué es el multiprocesamiento?

Multiprocesamiento Permite ejecutar varios procesos no relacionados simultáneamente. Estos procesos no comparten sus recursos y se comunican a través de IPC.

Python Múltiples subprocesos frente a multiprocesamiento

Para comprender los procesos y los subprocesos, considere el siguiente escenario: Un archivo .exe en su computadora es un programa. Al abrirlo, el sistema operativo lo carga en la memoria y la CPU lo ejecuta. La instancia del programa que se está ejecutando se denomina proceso.

Todo proceso tiene dos componentes fundamentales:

  • El Code
  • Los datos

Ahora, un proceso puede contener una o más subpartes llamadas roscas. Esto depende de la arquitectura del sistema operativo. Se puede pensar en un hilo como una sección del proceso que el sistema operativo puede ejecutar de forma independiente.

En otras palabras, se trata de una secuencia de instrucciones que el sistema operativo puede ejecutar de forma independiente. Los hilos dentro de un mismo proceso comparten los datos de dicho proceso y están diseñados para trabajar conjuntamente y facilitar el paralelismo.

¿Por qué utilizar subprocesos múltiples?

El multihilo permite dividir una aplicación en varias subtareas y ejecutarlas simultáneamente. Si utiliza el multihilo correctamente, podrá mejorar la velocidad, el rendimiento y la representación de su aplicación.

Python multihilo

Python Admite construcciones tanto para multiprocesamiento como para multihilo. En este tutorial, se centrará principalmente en la implementación. multiproceso aplicaciones con Python. Hay dos módulos principales que se pueden usar para manejar hilos en Python:

  1. El thread módulo, y
  2. El enhebrar módulo

Sin embargo, en PythonTambién existe algo llamado bloqueo global del intérprete (GIL). No permite mucha mejora del rendimiento e incluso puede sustancialmente el rendimiento de algunas aplicaciones multiproceso. Aprenderá todo al respecto en las próximas secciones de este tutorial.

Los módulos Thread y Threading

Los dos módulos que aprenderá en este tutorial son los módulo de hilo y conectar módulo de roscado.

Sin embargo, el módulo de subprocesos ha quedado obsoleto durante mucho tiempo. comenzando con Python 3, ha sido designado como obsoleto y sólo es accesible como _hilo para compatibilidad con versiones anteriores.

Deberías usar el nivel superior. enhebrar Módulo para las aplicaciones que planea implementar. El módulo thread se ha tratado aquí únicamente con fines didácticos.

El módulo de hilo

La sintaxis para crear un nuevo hilo usando este módulo es la siguiente:

thread.start_new_thread(function_name, arguments)

Muy bien, ahora has cubierto la teoría básica para comenzar a codificar. Entonces, abre tu IDLE o un bloc de notas y escribe lo siguiente:

import time
import _thread

def thread_test(name, wait):
   i = 0
   while i <= 3:
      time.sleep(wait)
      print("Running %s\n" %name)
      i = i + 1

   print("%s has finished execution" %name)

if __name__ == "__main__":
    
    _thread.start_new_thread(thread_test, ("First Thread", 1))
    _thread.start_new_thread(thread_test, ("Second Thread", 2))
    _thread.start_new_thread(thread_test, ("Third Thread", 3))

Guarde el archivo y presione F5 para ejecutar el programa. Si todo se hizo correctamente, este es el resultado que debería ver:

El módulo de hilo

En las próximas secciones aprenderás más sobre las condiciones de la carrera y cómo afrontarlas.

El módulo de hilo

EXPLICACIÓN DEL CÓDIGO

  1. Estas declaraciones importan el módulo de tiempo y de subprocesos, que se utilizan para gestionar la ejecución y el retraso de las operaciones. Python roscas.
  2. Aquí, ha definido una función llamada prueba_hilo, que será llamado por el inicio_nuevo_hilo Método. La función ejecuta un bucle while durante cuatro iteraciones e imprime el nombre del hilo que la llamó. Una vez completada la iteración, imprime un mensaje que indica que el hilo ha finalizado su ejecución.
  3. Esta es la sección principal de su programa. Aquí simplemente llama al inicio_nuevo_hilo método con el prueba_hilo Se pasa la función como argumento. Esto creará un nuevo hilo para la función y comenzará a ejecutarla. Tenga en cuenta que puede reemplazar `thread_test` con cualquier otra función que desee ejecutar como un hilo.

El módulo de enhebrado

Este módulo es la implementación de alto nivel de subprocesos en Python y el estándar de facto para la gestión de aplicaciones multihilo. Ofrece una amplia gama de funcionalidades en comparación con el módulo de hilos.

Estructura del módulo Threading

Estructura del módulo Threading

Aquí hay una lista de algunas funciones útiles definidas en este módulo:

Nombre de la función Mareas Ideales para Lecciones
cuentaActiva() Devuelve el conteo de Hilo objetos que aún están vivos.
subproceso actual() Devuelve el objeto actual de la clase Thread.
enumerar() Enumera todos los objetos Thread activos.
es demonio() Devuelve verdadero si el hilo es un demonio.
isAlive () Devuelve verdadero si el hilo aún está vivo.
Métodos de clase de hilo
comienzo() Inicia la actividad de un hilo. Se debe llamar solo una vez para cada subproceso porque generará un error de ejecución si se llama varias veces.
correr() Este método denota la actividad de un subproceso y puede ser anulado por una clase que extienda la clase Thread.
unirse() Bloquea la ejecución de otro código hasta que finaliza el hilo en el que se llamó al método join().

Historia de fondo: la clase de hilo

Antes de comenzar a codificar programas multihilo utilizando el módulo threading, es crucial comprender la clase Thread. La clase thread es la clase principal que define la plantilla y las operaciones de un hilo en Python.

La forma más común de crear un multihilo Python La aplicación consiste en declarar una clase que extienda la clase Thread y sobrescriba su método run().

La clase Thread, en resumen, significa una secuencia de código que se ejecuta en un thread de control.

Entonces, al escribir una aplicación multiproceso, harás lo siguiente:

  1. define una clase que extiende la clase Thread
  2. Anular el __init__ constructor
  3. Anular el correr() Método

Una vez que se ha creado un objeto de hilo, el comienzo() El método puede utilizarse para iniciar la ejecución de esta actividad, y el unirse() El método se puede utilizar para bloquear todo el resto del código hasta que finalice la actividad actual.

Ahora, intentemos usar el módulo threading para implementar su ejemplo anterior. Nuevamente, inicie su IDLE y escribe lo siguiente:

import time
import threading

class threadtester (threading.Thread):
    def __init__(self, id, name, i):
       threading.Thread.__init__(self)
       self.id = id
       self.name = name
       self.i = i
       
    def run(self):
       thread_test(self.name, self.i, 5)
       print ("%s has finished execution " %self.name)

def thread_test(name, wait, i):

    while i:
       time.sleep(wait)
       print ("Running %s \n" %name)
       i = i - 1

if __name__=="__main__":
    thread1 = threadtester(1, "First Thread", 1)
    thread2 = threadtester(2, "Second Thread", 2)
    thread3 = threadtester(3, "Third Thread", 3)

    thread1.start()
    thread2.start()
    thread3.start()

    thread1.join()
    thread2.join()
    thread3.join()

Este será el resultado cuando ejecutes el código anterior:

Historia de fondo: la clase de hilo

EXPLICACIÓN DEL CÓDIGO

Historia de fondo: la clase de hilo

  1. Esta parte es igual que nuestro ejemplo anterior. Aquí, importas el módulo de tiempo y el módulo de hilos, que se utilizan para manejar la ejecución y los retrasos de la Python roscas.
  2. En este bit, estás creando una clase llamada threadtester, que hereda o extiende el Hilo clase del módulo threading. Esta es una de las formas más comunes de crear hilos en Python. Sin embargo, solo debe sobrescribir el constructor y el correr() método en su aplicación. Como puede ver en el ejemplo de código anterior, el __init__ El método (constructor) ha sido anulado. Del mismo modo, también ha anulado el correr() método. Contiene el código que desea ejecutar dentro de un hilo. En este ejemplo, ha llamado a la función thread_test().
  3. Este es el método thread_test(), que toma el valor de i como argumento, lo disminuye en 1 en cada iteración y recorre el resto del código hasta que i se convierte en 0. En cada iteración, imprime el nombre del hilo que se está ejecutando actualmente y espera durante wait segundos (que también se toma como argumento).
  4. thread1 = threadtester(1, “First Thread”, 1) Aquí, estamos creando un hilo y pasando los tres parámetros que declaramos en __init__. El primer parámetro es la identificación del hilo, el segundo parámetro es el nombre del hilo y el tercer parámetro es el contador, que determina cuántas veces debe ejecutarse el ciclo while.
  5. thread2.start() El método start se utiliza para iniciar la ejecución de un hilo. Internamente, la función start() llama al método run() de su clase.
  6. thread3.join() El método join() bloquea la ejecución de otro código y espera hasta que finalice el hilo en el que fue llamado.

Como ya sabrás, los hilos que pertenecen al mismo proceso tienen acceso a la memoria y los datos de dicho proceso. Por lo tanto, si más de un hilo intenta modificar o acceder a los datos simultáneamente, pueden producirse errores.

En la siguiente sección, verá los diferentes tipos de complicaciones que pueden aparecer cuando los subprocesos acceden a los datos y la sección crítica sin comprobar si existen transacciones de acceso.

Puntos muertos y condiciones de carrera

Antes de aprender sobre interbloqueos y condiciones de carrera, será útil comprender algunas definiciones básicas relacionadas con la programación concurrente:

  • Sección crítica: Se trata de un fragmento de código que accede o modifica variables compartidas y debe ejecutarse como una transacción atómica.
  • Cambio de contexto: Es el proceso que sigue una CPU para almacenar el estado de un hilo antes de cambiar de una tarea a otra, de modo que pueda reanudarse desde el mismo punto más adelante.

Puntos muertos

Puntos muertos son el problema más temido al que se enfrentan los desarrolladores al escribir aplicaciones concurrentes/multihilo en PythonLa mejor manera de entender los interbloqueos es utilizando el problema clásico de la informática conocido como el Gastronomía PhiloProblema de Sophers.

El planteamiento del problema para los filósofos comedores es el siguiente:

Cinco filósofos están sentados alrededor de una mesa redonda con cinco platos de espaguetis (un tipo de pasta) y cinco tenedores, como se muestra en el diagrama.

Gastronomía Philoproblema de sophers

Gastronomía Philoproblema de sophers

En cualquier momento dado, un filósofo debe estar comiendo o pensando.

Además, un filósofo debe coger los dos tenedores adyacentes (es decir, el izquierdo y el derecho) antes de poder comer los espaguetis. El problema del punto muerto se produce cuando los cinco filósofos cogen simultáneamente sus tenedores derechos.

Como cada filósofo tiene un tenedor, todos esperarán a que los demás dejen el suyo. Como resultado, ninguno de ellos podrá comer espaguetis.

De manera similar, en un sistema concurrente, se produce un bloqueo cuando diferentes subprocesos o procesos (filósofos) intentan adquirir los recursos compartidos del sistema (bifurcaciones) al mismo tiempo. Como resultado, ninguno de los procesos tiene la oportunidad de ejecutarse, ya que están esperando otro recurso que esté en poder de otro proceso.

Condiciones de carrera

Una condición de carrera es un estado no deseado de un programa que ocurre cuando un sistema realiza dos o más operaciones simultáneamente. Por ejemplo, considere este sencillo bucle for:

i=0; # a global variable
for x in range(100):
    print(i)
    i+=1;

Si creas n Aunque varios hilos ejecutan este código simultáneamente, no es posible determinar el valor de i (que es compartido por los hilos) cuando el programa finaliza su ejecución. Esto se debe a que, en un entorno multihilo real, los hilos pueden superponerse, y el valor de i, que fue recuperado y modificado por un hilo, puede cambiar mientras otro hilo accede a él.

Estas son las dos clases principales de problemas que pueden ocurrir en un sistema multihilo o distribuido. Python aplicación. En la siguiente sección, aprenderá cómo superar este problema sincronizando los hilos.

Synchilos cronizadores

Para lidiar con condiciones de carrera, bloqueos y otros problemas relacionados con subprocesos, el módulo de subprocesos proporciona la Bloquear objeto. La idea es que cuando un subproceso desea acceder a un recurso específico, adquiere un bloqueo para ese recurso. Una vez que un subproceso bloquea un recurso en particular, ningún otro subproceso puede acceder a él hasta que se libere el bloqueo. Como resultado, los cambios en el recurso serán atómicos y se evitarán las condiciones de carrera.

Un bloqueo es una primitiva de sincronización de bajo nivel implementada por el _hilo módulo. En cualquier momento dado, un bloqueo puede estar en uno de dos estados: cerrado or desbloqueado Admite dos métodos:

  1. adquirir(): Cuando el estado de bloqueo se desbloquea, al llamar al método acquire() se cambia el estado a bloqueado y se devuelve un valor. Sin embargo, si el estado es bloqueado, la llamada a acquire() se bloquea hasta que otro hilo llame al método release().
  2. liberar(): El método release() se utiliza para establecer el estado en desbloqueado, es decir, para liberar un bloqueo. Puede ser llamado por cualquier hilo, no necesariamente por el que adquirió el bloqueo.

Aquí tienes un ejemplo de cómo usar bloqueos en tus aplicaciones. Inicia tu IDLE y escriba lo siguiente:

import threading
lock = threading.Lock()

def first_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the first funcion')
        lock.release()

def second_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the second funcion')
        lock.release()

if __name__=="__main__":
    thread_one = threading.Thread(target=first_function)
    thread_two = threading.Thread(target=second_function)

    thread_one.start()
    thread_two.start()

    thread_one.join()
    thread_two.join()

Ahora presiona F5. Deberías ver un resultado como este:

SyncHilos de cronización

EXPLICACIÓN DEL CÓDIGO

SyncHilos de cronización

  1. Aquí, simplemente estás creando un nuevo bloqueo llamando al threading.Lock () Función de fábrica. Internamente, Lock() devuelve una instancia de la clase Lock concreta más eficaz mantenida por la plataforma.
  2. En la primera declaración, adquiere el bloqueo llamando al método adquirir(). Cuando se ha concedido el bloqueo, imprime “bloqueo adquirido” a la consola. Una vez que todo el código que desea que ejecute el subproceso haya finalizado su ejecución, libere el bloqueo llamando al método release().

La teoría es correcta, pero ¿cómo saber si el bloqueo funcionó correctamente? Si observas la salida, verás que cada instrucción `print` imprime exactamente una línea a la vez. Recuerda que, en un ejemplo anterior, las salidas de `print` eran aleatorias porque varios hilos accedían al método `print()` simultáneamente. Aquí, la función `print` se llama solo después de adquirir el bloqueo. Por lo tanto, las salidas se muestran una a una y línea por línea.

Aparte de las cerraduras, Python También admite otros mecanismos para gestionar la sincronización de subprocesos, como se indica a continuación:

  1. Bloqueos
  2. Semaphores
  3. Condiciones
  4. Eventos, y
  5. Barreras

Bloqueo global de intérprete (y cómo solucionarlo)

Antes de entrar en los detalles de PythonPara entender mejor la siguiente sección, definamos algunos términos que serán útiles:

  1. Código que depende de la CPU: esto se refiere a cualquier fragmento de código que será ejecutado directamente por la CPU.
  2. Código con restricciones de E/S: puede tratarse de cualquier código que acceda al sistema de archivos a través del sistema operativo.
  3. CPython: es la referencia implementación of Python y puede describirse como el intérprete escrito en C y Python (lenguaje de programación).

¿Qué es GIL en Python?

Bloqueo de intérprete global (GIL) in Python es un bloqueo de proceso o un mutex que se utiliza al tratar con los procesos. Asegura que un hilo pueda acceder a un recurso particular a la vez, y también impide el uso simultáneo de objetos y bytecodes. Esto beneficia a los programas de un solo hilo con un aumento del rendimiento. GIL en Python Es muy sencillo y fácil de implementar.

Se puede utilizar un bloqueo para garantizar que solo un subproceso tenga acceso a un recurso particular en un momento dado.

Una de las características de Python es que utiliza un bloqueo global en cada proceso intérprete, lo que significa que cada proceso trata el Python el intérprete en sí mismo como recurso.

Por ejemplo, supongamos que has escrito un Python Programa que utiliza dos hilos para realizar operaciones de CPU y de entrada/salida. Al ejecutar este programa, sucede lo siguiente:

  1. El Python El intérprete crea un nuevo proceso y genera los hilos.
  2. Cuando el subproceso 1 comience a ejecutarse, primero adquirirá el GIL y lo bloqueará.
  3. Si el subproceso 2 quiere ejecutarse ahora, tendrá que esperar a que se libere el GIL incluso si hay otro procesador libre.
  4. Ahora, supongamos que el subproceso 1 está esperando una operación de E/S. En ese momento, liberará el GIL y el subproceso 2 lo adquirirá.
  5. Después de completar las operaciones de E/S, si el subproceso 1 quiere ejecutarse ahora, tendrá que esperar nuevamente a que el subproceso 2 libere el GIL.

Debido a esto, solo un hilo puede acceder al intérprete en cualquier momento, lo que significa que solo habrá un hilo ejecutándose. Python código en un momento dado.

Esto no supone ningún problema en un procesador de un solo núcleo, ya que se utilizaría la segmentación de tiempo (véase la primera sección de este tutorial) para gestionar los hilos. Sin embargo, en el caso de procesadores multinúcleo, una función que consume muchos recursos de la CPU y se ejecuta en varios hilos tendrá un impacto considerable en la eficiencia del programa, puesto que no utilizará todos los núcleos disponibles simultáneamente.

¿Por qué se necesitaba GIL?

El CPython El recolector de basura utiliza una técnica eficiente de administración de memoria conocida como conteo de referencias. Así es como funciona: Cada objeto en Python Un objeto tiene un contador de referencias, que aumenta al asignarle un nuevo nombre de variable o al agregarlo a un contenedor (como tuplas, listas, etc.). Del mismo modo, el contador de referencias disminuye cuando la referencia sale del ámbito o cuando se llama a la instrucción `del`. Cuando el contador de referencias de un objeto llega a cero, se elimina mediante el recolector de basura y se libera la memoria asignada.

Pero el problema es que la variable de conteo de referencias es propensa a condiciones de carrera como cualquier otra variable global. Para resolver este problema, los desarrolladores de Python Se decidió utilizar el bloqueo global del intérprete. La otra opción era añadir un bloqueo a cada objeto, lo que habría provocado interbloqueos y un aumento de la sobrecarga derivada de las llamadas a acquire() y release().

Por lo tanto, GIL es una restricción significativa para el multihilo. Python programas que ejecutan operaciones intensivas que dependen de la CPU (lo que efectivamente los convierte en un solo hilo). Si desea utilizar varios núcleos de CPU en su aplicación, utilice el multiprocesamiento módulo en su lugar.

Preguntas Frecuentes

Utilice subprocesos para tareas que requieren mucha E/S, como llamadas de red, acceso a archivos o consultas a bases de datos, donde los subprocesos esperan recursos externos. Utilice multiprocesamiento para tareas que requieren mucha CPU, como cálculos complejos, ya que se ejecuta en varios núcleos y evita el GIL (Global Interpreter Lock).

No por defecto. PEP 703 introdujo una compilación opcional de subprocesos libres en Python 3.13 que desactiva el GIL y Python La versión 3.14 continúa con esta práctica. Las compilaciones estándar aún incluyen el GIL, por lo que la mayoría de los programas siguen funcionando exactamente igual que antes.

Un hilo demonio se ejecuta en segundo plano y no impide que el programa finalice. Cuando solo quedan hilos demonio, Python Los apaga y sale. Configure uno con thread.daemon = True antes de llamar a start().

La clase Thread no devuelve el resultado de run() directamente. Almacene el valor en el objeto thread o en una cola compartida, o utilice concurrent.futures.ThreadPoolExecutor, cuyo método submit() devuelve un Future que se lee con result().

ThreadPoolExecutor es una función auxiliar de alto nivel que gestiona un grupo de subprocesos de trabajo. Se envían tareas con submit() o map(), y esta función se encarga de la creación, reutilización y limpieza de los subprocesos, devolviendo objetos Future que contienen cada resultado.

Puedes crear cientos o miles de hilos, pero el GIL solo permite que se ejecute uno. Python código de bytes en cualquier instante. Todavía se superponen durante las esperas de E/S, por lo que muchos hilos concurrentes ayudan mucho más a los programas limitados por E/S que a los limitados por la CPU.

Sí, principalmente para pasos que dependen de E/S, como cargar datos, llamar a API o leer archivos. El entrenamiento intensivo depende de la CPU, por lo que se utilizan bibliotecas como NumPy y Py.TorLibere el GIL en el código C o confíe en el multiprocesamiento para lograr un verdadero paralelismo.

Sí. GitHub Copilot autocompleta patrones comunes como subclases de Thread, uso de Lock y configuraciones de ThreadPoolExecutor a partir de un comentario o nombre de función. Siempre revise el código generado para detectar condiciones de carrera y el manejo correcto de los bloqueos antes de usarlo.

Resumir este post con: