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.
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:
- El thread mĂłdulo, y
- 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:
En las prĂłximas secciones aprenderĂĄs mĂĄs sobre las condiciones de la carrera y cĂłmo afrontarlas.
EXPLICACIĂN DEL CĂDIGO
- 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.
- 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.
- 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
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:
- define una clase que extiende la clase Thread
- Anular el __init__ constructor
- 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:
EXPLICACIĂN DEL CĂDIGO
- 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.
- 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().
- 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).
- 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.
- 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.
- 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
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:
- 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().
- 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:
EXPLICACIĂN DEL CĂDIGO
- 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.
- 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:
- Bloqueos
- Semaphores
- Condiciones
- Eventos, y
- 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:
- CĂłdigo que depende de la CPU: esto se refiere a cualquier fragmento de cĂłdigo que serĂĄ ejecutado directamente por la CPU.
- 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.
- 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:
- El Python El intérprete crea un nuevo proceso y genera los hilos.
- Cuando el subproceso 1 comience a ejecutarse, primero adquirirĂĄ el GIL y lo bloquearĂĄ.
- Si el subproceso 2 quiere ejecutarse ahora, tendrĂĄ que esperar a que se libere el GIL incluso si hay otro procesador libre.
- 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ĂĄ.
- 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.









