subprocesos múltiples en Python con ejemplo: Aprenda GIL en Python
¿Qué es un hilo?
Un hilo es una unidad de ejecución en programación concurrente. Multithreading es una técnica que permite a una CPU ejecutar muchas tareas de un proceso al mismo tiempo. Estos subprocesos pueden ejecutarse individualmente mientras comparten sus recursos de proceso.
¿Qué es un proceso?
Un proceso es básicamente el programa en ejecución. Cuando inicias una aplicación en tu computadora (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 es una técnica bien conocida en la que varios subprocesos de un proceso comparten su espacio de datos con el subproceso principal, lo que hace que el intercambio de información y la comunicación dentro de los subprocesos sean fáciles y eficientes. Los hilos son más ligeros que los procesos. Varios subprocesos pueden ejecutarse individualmente mientras comparten sus recursos de proceso. El propósito del subproceso múltiple es ejecutar múltiples tareas y celdas de funciones al mismo tiempo.
¿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 subprocesos, considere este escenario: un archivo .exe en su computadora es un programa. Cuando lo abres, el sistema operativo lo carga en la memoria y la CPU lo ejecuta. La instancia del programa que se está ejecutando ahora se llama proceso.
Todo proceso tendrá 2 componentes fundamentales:
- El código
- Los datos
Ahora, un proceso puede contener una o más subpartes llamadas roscas. Esto depende de la arquitectura del sistema operativo. Puedes pensar en un hilo como una sección del proceso que el sistema operativo puede ejecutar por separado.
En otras palabras, es un flujo de instrucciones que el sistema operativo puede ejecutar de forma independiente. Los subprocesos dentro de un solo proceso comparten los datos de ese proceso y están diseñados para trabajar juntos para 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 Multiproceso
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 subprocesos en Python:
- Un espacio para hacer una pausa, reflexionar y reconectarse en privado. thread módulo, y
- Un espacio para hacer una pausa, reflexionar y reconectarse en privado. enhebrar módulo
Sin embargo, en Python, también existe algo llamado bloqueo de intérprete global (GIL). No permite mucho aumento de rendimiento e incluso puede reducir 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 así 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 desea implementar. El módulo del hilo sólo se ha cubierto aquí con fines educativos.
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:
Aprenderás más sobre las condiciones de carrera y cómo manejarlas en las próximas secciones.
EXPLICACIÓN DEL CÓDIGO
- Estas declaraciones importan el módulo de tiempo y subproceso que se utilizan para manejar la ejecución y el retraso del 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 lo llamó. Una vez que se completa la iteración, imprime un mensaje que dice 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 funciona como argumento. Esto creará un nuevo hilo para la función que pasas como argumento y comenzará a ejecutarla. Tenga en cuenta que puede reemplazar esto (hilo_test) con cualquier otra función que desee ejecutar como 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 administrar aplicaciones multiproceso. Proporciona una amplia gama de funciones en comparación con el módulo de subprocesos.
Aquí hay una lista de algunas funciones útiles definidas en este módulo:
Nombre de la función | Descripción |
---|---|
cuentaActiva() | Devuelve el conteo de Hilo objetos que todavía 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 multiproceso utilizando el módulo de subprocesos, es fundamental 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 una aplicación Python multiproceso es declarar una clase que extiende la clase Thread y anula 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:
- definir una clase que extienda la clase Thread
- Anular el __init__ constructor
- Anular el correr() Método
Una vez que se ha creado un objeto de hilo, el comienzo() método se puede utilizar para comenzar la ejecución de esta actividad y la 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 de subprocesamiento para implementar su ejemplo anterior. De nuevo, enciende tu 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 subproceso que se utilizan para gestionar 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 de subprocesamiento. Esta es una de las formas más comunes de crear hilos en Python. Sin embargo, sólo debe anular 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 duerme durante los segundos de espera (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 sabes, los subprocesos que están en el mismo proceso tienen acceso a la memoria y a los datos de ese proceso. Por lo tanto, si más de un subproceso intenta cambiar o acceder a los datos simultáneamente, pueden aparecer errores.
En la siguiente sección, verá los diferentes tipos de complicaciones que pueden aparecer cuando los subprocesos acceden a los datos y a la sección crítica sin verificar las transacciones de acceso existentes.
Puntos muertos y condiciones de carrera
Antes de aprender sobre los bloqueos y las condiciones de carrera, será útil comprender algunas definiciones básicas relacionadas con la programación concurrente:
- Sección críticaEs un fragmento de código que accede o modifica variables compartidas y debe realizarse como una transacción atómica.
- Cambio de contextoEs el proceso que sigue una CPU para almacenar el estado de un hilo antes de cambiar de una tarea a otra para que pueda reanudarse desde el mismo punto más tarde.
Puntos muertos
Puntos muertos son el problema más temido que enfrentan los desarrolladores al escribir aplicaciones concurrentes o multiproceso en Python. La mejor manera de entender los interbloqueos es usar el clásico problema de ejemplo de la informática conocido como Gastronomía PhiloProblema de Sophers.
El planteamiento del problema para los filósofos comedores es el siguiente:
Cinco filósofos están sentados en una mesa redonda con cinco platos de espaguetis (un tipo de pasta) y cinco tenedores, como se muestra en el diagrama.
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 se produce 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 número de subprocesos que ejecutan este código a la vez, no puede determinar el valor de i (que es compartido por los subprocesos) cuando el programa finaliza la ejecución. Esto se debe a que en un entorno real de subprocesos múltiples, los subprocesos pueden superponerse y el valor de i que fue recuperado y modificado por un subproceso puede cambiar cuando otro subproceso accede a él.
Estos son los dos tipos principales de problemas que pueden ocurrir en una aplicación de Python distribuida o multiproceso. En la siguiente sección, aprenderá a solucionar este problema sincronizando los subprocesos.
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, una cerradura puede estar en uno de 2 estados: cerrado or desbloqueado Admite dos métodos:
- adquirir()Cuando el estado de bloqueo está desbloqueado, llamar al método adquirir() cambiará el estado a bloqueado y regresará. Sin embargo, si el estado está bloqueado, la llamada a adquirir() se bloquea hasta que algún otro subproceso llame al método release().
- lanzamiento()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.
A continuación se muestra un ejemplo del uso de bloqueos en sus aplicaciones. Enciende 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 está bien, pero ¿cómo sabes que el bloqueo realmente funcionó? Si miras el resultado, verás que cada una de las sentencias de impresión imprime exactamente una línea a la vez. Recuerda que, en un ejemplo anterior, los resultados de la impresión eran aleatorios porque varios subprocesos accedían al método print() al mismo tiempo. Aquí, la función de impresión se llama solo después de que se adquiere el bloqueo. Por lo tanto, los resultados se muestran uno a la vez y línea por línea.
Además de los bloqueos, Python también admite otros mecanismos para gestionar la sincronización de subprocesos, como se enumeran a continuación:
- Bloqueos
- Semaphores
- Condiciones
- Eventos, y
- Barreras
Bloqueo global de intérprete (y cómo solucionarlo)
Antes de entrar en los detalles del GIL de Python, definamos algunos términos que serán útiles para comprender la próxima sección:
- Código vinculado a la CPU: se refiere a cualquier fragmento de código que será ejecutado directamente por la CPU.
- Código vinculado a E/S: puede ser 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) en Python hay un bloqueo de proceso o un mutex que se utiliza al tratar con los procesos. Garantiza que un hilo pueda acceder a un recurso particular a la vez y también evita el uso de objetos y códigos de bytes a la vez. Esto beneficia a los programas de un solo subproceso en un aumento de rendimiento. GIL en Python es muy simple 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 de intérprete, lo que significa que cada proceso trata al intérprete de Python en sí mismo como un recurso.
Por ejemplo, supongamos que ha escrito un programa de Python que utiliza dos subprocesos para realizar operaciones de CPU y de E/S. Cuando ejecuta este programa, esto es lo que sucede:
- El intérprete de Python 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 ejecutando código Python en un momento dado.
Esto está bien en un procesador de un solo núcleo porque usaría división de tiempo (consulte la primera sección de este tutorial) para manejar los subprocesos. Sin embargo, en el caso de procesadores multinúcleo, una función vinculada a la CPU que se ejecuta en múltiples subprocesos tendrá un impacto considerable en la eficiencia del programa, ya que en realidad no utilizará todos los núcleos disponibles al mismo tiempo.
¿Por qué se necesitaba GIL?
El CPython El recolector de basura utiliza una técnica de administración de memoria eficiente conocida como recuento de referencias. Así es como funciona: cada objeto en Python tiene un recuento de referencias, que aumenta cuando se le asigna un nuevo nombre de variable o se agrega a un contenedor (como tuplas, listas, etc.). Del mismo modo, el recuento de referencias disminuye cuando la referencia sale del ámbito o cuando se llama a la declaración del. Cuando el recuento de referencias de un objeto llega a 0, se recolecta basura y se libera la memoria asignada.
Pero el problema es que la variable de recuento de referencias es propensa a condiciones de carrera como cualquier otra variable global. Para resolver este problema, los desarrolladores de Python decidieron utilizar el bloqueo del intérprete global. La otra opción era agregar un bloqueo a cada objeto, lo que habría resultado en bloqueos y mayor sobrecarga de las llamadas acquire() y release().
Por lo tanto, GIL es una restricción importante para los programas Python multiproceso que ejecutan operaciones pesadas limitadas por la CPU (lo que los convierte en un solo subproceso). Si desea utilizar varios núcleos de CPU en su aplicación, use el multiprocesamiento módulo en su lugar.
Resumen
- Python admite 2 módulos para subprocesos múltiples:
- __hilo módulo: proporciona una implementación de bajo nivel para subprocesos y está obsoleto.
- módulo de roscado: Proporciona una implementación de alto nivel para subprocesos múltiples y es el estándar actual.
- Para crear un hilo utilizando el módulo de subprocesos, debes hacer lo siguiente:
- Crea una clase que extienda el Hilo clase.
- Anula su constructor (__init__).
- Anular su correr() método.
- Crea un objeto de esta clase.
- Un hilo se puede ejecutar llamando al comienzo() método.
- Un espacio para hacer una pausa, reflexionar y reconectarse en privado. unirse() El método se puede utilizar para bloquear otros subprocesos hasta que este subproceso (en el que se llamó a join) finalice la ejecución.
- Una condición de carrera ocurre cuando varios subprocesos acceden o modifican un recurso compartido al mismo tiempo.
- Se puede evitar mediante SyncHilos cronizadores.
- Python Admite 6 formas de sincronizar hilos:
- Candados
- Bloqueos
- Semaphores
- Condiciones
- Eventos, y
- Barreras
- Los bloqueos permiten que sólo un hilo particular que ha adquirido el bloqueo ingrese a la sección crítica.
- Un candado tiene 2 métodos principales:
- adquirir(): Establece el estado de bloqueo en bloqueado Si se llama a un objeto bloqueado, se bloquea hasta que el recurso esté libre.
- lanzamiento(): Establece el estado de bloqueo en desbloqueado y regresa. Si se llama a un objeto desbloqueado, devuelve falso.
- El bloqueo global del intérprete es un mecanismo a través del cual solo 1 CPython El proceso del intérprete se puede ejecutar a la vez.
- Se utilizó para facilitar la funcionalidad de recuento de referencias de C.PythonEl recolector de basura de s.
- Para hacer Python Para aplicaciones con operaciones que exigen mucho uso de la CPU, debe utilizar el módulo de multiprocesamiento.