El lenguaje de programación Python le permite utilizar multiprocesamiento o multiproceso. En este tutorial, aprenderá a escribir aplicaciones multiproceso en Python.
¿Qué es un hilo?
Un hilo es una unidad de ejecución en programación concurrente. El subproceso múltiple 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 inicia una aplicación en su computadora (como un navegador o editor de texto), el sistema operativo crea un proceso.
¿Qué es el subproceso múltiple en Python?
El subproceso múltiple en la programación Python es una técnica bien conocida en la que varios subprocesos en 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 sea fácil y eficiente. Los hilos son más ligeros que los procesos. Los subprocesos múltiples pueden ejecutarse individualmente mientras comparten sus recursos de proceso. El propósito del subproceso múltiple es ejecutar múltiples tareas y celdas de función al mismo tiempo.
¿Qué es el multiprocesamiento?
El multiprocesamiento le permite ejecutar simultáneamente varios procesos no relacionados. Estos procesos no comparten sus recursos y se comunican a través de IPC.
Python multiproceso frente a multiprocesamiento
Para comprender los procesos y los subprocesos, considere este escenario: Un archivo .exe en su computadora es un programa. Cuando lo abre, 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.
Cada proceso tendrá 2 componentes fundamentales:
- El código
- Los datos
Ahora, un proceso puede contener una o más subpartes llamadas subprocesos. Esto depende de la arquitectura del sistema operativo. Puede 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.
En este tutorial, aprenderá,
- ¿Qué es un hilo?
- ¿Qué es un proceso?
- ¿Qué es el subproceso múltiple?
- ¿Qué es el multiprocesamiento?
- Python multiproceso frente a multiprocesamiento
- ¿Por qué utilizar Multithreading?
- Python MultiThreading
- Los módulos Thread y Threading
- El módulo de subprocesos
- El módulo de subprocesamiento
- Puntos muertos y condiciones de carrera
- Sincronizar hilos
- ¿Qué es GIL?
- ¿Por qué se necesitaba GIL?
¿Por qué utilizar Multithreading?
El subproceso múltiple le permite dividir una aplicación en múltiples subtareas y ejecutar estas tareas simultáneamente. Si utiliza el subproceso múltiple correctamente, la velocidad, el rendimiento y la representación de su aplicación se pueden mejorar.
Python MultiThreading
Python admite construcciones tanto para multiprocesamiento como para multiproceso. En este tutorial, se centrará principalmente en implementar aplicaciones multiproceso con python. Hay dos módulos principales que se pueden usar para manejar subprocesos en Python:
- El módulo de hilo , y
- El módulo de enhebrado
Sin embargo, en Python, también hay algo llamado bloqueo de intérprete global (GIL). No permite una gran ganancia de rendimiento e incluso puede reducir el rendimiento de algunas aplicaciones multiproceso. Lo aprenderá todo en las próximas secciones de este tutorial.
Los módulos Thread y Threading
Los dos módulos que aprenderá en este tutorial son el módulo de subprocesos y el módulo de subprocesos .
Sin embargo, el módulo de subprocesos ha quedado obsoleto durante mucho tiempo. A partir de Python 3, se ha designado como obsoleto y solo se puede acceder a él como __thread para compatibilidad con versiones anteriores.
Debe utilizar el módulo de subprocesos de nivel superior para las aplicaciones que desea implementar. El módulo de subprocesos solo se ha cubierto aquí con fines educativos.
El módulo de subprocesos
La sintaxis para crear un nuevo hilo usando este módulo es la siguiente:
thread.start_new_thread(function_name, arguments)
Muy bien, ahora ha cubierto la teoría básica para comenzar a codificar. Entonces, abra su IDLE o un bloc de notas y escriba lo siguiente:
import timeimport _threaddef thread_test(name, wait):i = 0while i <= 3:time.sleep(wait)print("Running %s\n" %name)i = i + 1print("%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á más sobre las condiciones de la 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 de los subprocesos de Python.
- Aquí, ha definido una función llamada thread_test, que será llamada por el método start_new_thread . La función ejecuta un ciclo 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 terminado de ejecutarse.
- Esta es la sección principal de su programa. Aquí, simplemente llame al método start_new_thread con la función thread_test como argumento.
Esto creará un nuevo hilo para la función que pasa como argumento y comenzará a ejecutarlo. Tenga en cuenta que puede reemplazar este (hilo _ prueba) con cualquier otra función que desee ejecutar como hilo.
El módulo de subprocesamiento
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 |
activeCount () | Devuelve el recuento de objetos Thread que aún están vivos. |
currentThread () | Devuelve el objeto actual de la clase Thread. |
enumerar() | Enumera todos los objetos Thread activos. |
isDaemon () | 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 hilo porque arrojará un error de tiempo de ejecución si se llama varias veces. |
correr() | Este método denota la actividad de un hilo y puede ser anulado por una clase que amplíe 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 (). |
Trasfondo: La clase Thread
Antes de comenzar a codificar programas multiproceso utilizando el módulo de subprocesos, es crucial comprender la clase Thread. La clase thread es la clase principal que define la plantilla y las operaciones de un subproceso en Python.
La forma más común de crear una aplicación de 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 hilo de control separado .
Entonces, al escribir una aplicación multiproceso, hará lo siguiente:
- definir una clase que amplíe la clase Thread
- Anular el constructor __init__
- Anular el método run ()
Una vez que se ha creado un objeto de hilo, el método start () se puede usar para comenzar la ejecución de esta actividad y el método join () se puede usar para bloquear el resto del código hasta que finalice la actividad actual.
Ahora, intentemos usar el módulo de subprocesos para implementar su ejemplo anterior. Nuevamente, encienda su IDLE y escriba lo siguiente:
import timeimport threadingclass threadtester (threading.Thread):def __init__(self, id, name, i):threading.Thread.__init__(self)self.id = idself.name = nameself.i = idef 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 - 1if __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()
Esta será la salida cuando ejecute el código anterior:
EXPLICACIÓN DEL CÓDIGO
- Esta parte es igual a nuestro ejemplo anterior. Aquí, importa el módulo de tiempo y subprocesos que se utilizan para manejar la ejecución y los retrasos de los subprocesos de Python.
- En este bit, está creando una clase llamada threadtester, que hereda o extiende la clase Thread del módulo de threading. Esta es una de las formas más comunes de crear hilos en Python. Sin embargo, solo debe anular el constructor y el método run () en su aplicación. Como puede ver en el ejemplo de código anterior, el método __init__ (constructor) ha sido anulado.
Del mismo modo, también ha anulado el método run () . 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 unos segundos de espera (que también se toma como argumento).
- thread1 = threadtester (1, "Primer hilo", 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 de inicio 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 sabe, los subprocesos que están en el mismo proceso tienen acceso a la memoria y los datos de ese proceso. Como resultado, si más de un hilo 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 hilos acceden a los datos y la sección crítica sin verificar las transacciones de acceso existentes.
Puntos muertos y condiciones de carrera
Antes de aprender sobre los puntos muertos y las condiciones de carrera, será útil comprender algunas definiciones básicas relacionadas con la programación concurrente:
- Sección crítica
Es un fragmento de código que accede o modifica variables compartidas y debe realizarse 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 para que pueda reanudarse desde el mismo punto más adelante.
Interbloqueos
Los interbloqueos son el problema más temido que enfrentan los desarrolladores al escribir aplicaciones concurrentes / multiproceso en Python. La mejor manera de entender los puntos muertos es utilizando el problema de ejemplo clásico de la informática conocido como el Problema de los filósofos de la comida.
El enunciado del problema para los filósofos gastronómicos es el siguiente:
Cinco filósofos están sentados en una mesa redonda con cinco platos de espagueti (un tipo de pasta) y cinco tenedores, como se muestra en el diagrama.
En un momento dado, un filósofo debe estar comiendo o pensando.
Además, un filósofo debe tomar los dos tenedores adyacentes a él (es decir, los tenedores izquierdo y derecho) antes de poder comer los espaguetis. El problema del estancamiento se produce cuando los cinco filósofos toman sus bifurcaciones derechas simultáneamente.
Dado que cada uno de los filósofos tiene un tenedor, todos esperarán a que los demás bajen el tenedor. Como resultado, ninguno de ellos podrá comer espaguetis.
De manera similar, en un sistema concurrente, se produce un punto muerto 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 mientras esperan otro recurso retenido por algún 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 simple bucle for:
i=0; # a global variablefor x in range(100):print(i)i+=1;
Si crea 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.
Estas son las dos clases principales de problemas que pueden ocurrir en una aplicación Python multiproceso o distribuida. En la siguiente sección, aprenderá cómo solucionar este problema sincronizando subprocesos.
Sincronizar hilos
Para lidiar con condiciones de carrera, interbloqueos y otros problemas basados en subprocesos, el módulo de subprocesos proporciona el objeto Lock . La idea es que cuando un hilo quiere acceder a un recurso específico, adquiere un bloqueo para ese recurso. Una vez que un hilo bloquea un recurso en particular, ningún otro hilo 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 módulo __thread . En cualquier momento, un candado puede estar en uno de dos estados: bloqueado o desbloqueado. Admite dos métodos:
- adquirir()
Cuando el estado de bloqueo está desbloqueado, llamar al método adquirido () cambiará el estado a bloqueado y regresará. Sin embargo, si el estado está bloqueado, la llamada a adquirir () se bloquea hasta que otro hilo llame al método release ().
- liberación()
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 el que adquirió el bloqueo.
A continuación, se muestra un ejemplo del uso de bloqueos en sus aplicaciones. Encienda su IDLE y escriba lo siguiente:
import threadinglock = 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, presione F5. Debería ver una salida como esta:
EXPLICACIÓN DEL CÓDIGO
- Aquí, simplemente está creando un nuevo bloqueo llamando a la función de fábrica threading.Lock () . Internamente, Lock () devuelve una instancia de la clase Lock concreta más eficaz que mantiene la plataforma.
- En la primera declaración, adquiere el bloqueo llamando al método adquirido (). Cuando se ha concedido el candado, imprime "candado adquirido" en la consola. Una vez que todo el código que desea que ejecute el hilo ha terminado de ejecutarse, libera el bloqueo llamando al método release ().
La teoría está bien, pero ¿cómo sabes que la cerradura realmente funcionó? Si observa la salida, verá que cada una de las declaraciones impresas se imprime exactamente una línea a la vez. Recuerde que, en un ejemplo anterior, las salidas de print eran desordenadas porque varios subprocesos estaban accediendo 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. Entonces, las salidas se muestran una a la vez y línea por línea.
Además de los bloqueos, Python también admite algunos otros mecanismos para manejar la sincronización de subprocesos como se enumera a continuación:
- RCerraduras
- Semáforos
- Condiciones
- Eventos y
- Barreras
Bloqueo de intérprete global (y cómo lidiar con él)
Antes de entrar en los detalles de 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 implementación de referencia de Python y se puede describir como el intérprete escrito en C y Python (lenguaje de programación).
¿Qué es GIL en Python?
Global Interpreter Lock (GIL) en python es un bloqueo de proceso o un mutex que se utiliza al tratar con los procesos. Se asegura de que un hilo pueda acceder a un recurso en 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 usar un bloqueo para asegurarse de que solo un subproceso tenga acceso a un recurso en particular en un momento dado.
Una de las características de Python es que usa un bloqueo global en cada proceso de intérprete, lo que significa que cada proceso trata al intérprete de Python como un recurso.
Por ejemplo, suponga que ha escrito un programa en Python que usa dos subprocesos para realizar operaciones de CPU y '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 thread-1 comienza a ejecutarse, primero adquirirá el GIL y lo bloqueará.
- Si thread-2 quiere ejecutarse ahora, tendrá que esperar a que se libere el GIL incluso si otro procesador está libre.
- Ahora, suponga que el subproceso 1 está esperando una operación de E / S. En este momento, lanzará el GIL y el hilo-2 lo adquirirá.
- Después de completar las operaciones de E / S, si el thread-1 quiere ejecutarse ahora, nuevamente tendrá que esperar a que el thread-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 la división de tiempo (consulte la primera sección de este tutorial) para manejar los subprocesos. Sin embargo, en el caso de los procesadores de varios núcleos, una función vinculada a la CPU que se ejecuta en varios 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 recolector de basura CPython 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 asigna a un nuevo nombre de variable o se agrega a un contenedor (como tuplas, listas, etc.). Asimismo, el recuento de referencias se reduce cuando la referencia sale del alcance o cuando se llama a la instrucción del. Cuando el recuento de referencias de un objeto llega a 0, se recolecta la basura y se libera la memoria asignada.
Pero el problema es que la variable de recuento de referencia es propensa a condiciones de carrera como cualquier otra variable global. Para resolver este problema, los desarrolladores de Python decidieron utilizar el bloqueo de intérprete global. La otra opción era agregar un bloqueo a cada objeto, lo que habría dado lugar a interbloqueos y un aumento de la sobrecarga de las llamadas de adquisición () y liberación ().
Por lo tanto, GIL es una restricción significativa para los programas de Python multiproceso que ejecutan operaciones pesadas vinculadas a la CPU (lo que los hace de manera efectiva de un solo subproceso). Si desea utilizar varios núcleos de CPU en su aplicación, utilice el módulo de multiprocesamiento en su lugar.
Resumen
- Python admite 2 módulos para subprocesos múltiples:
- Módulo __thread : proporciona una implementación de bajo nivel para subprocesos y es obsoleto.
- Módulo de subprocesos : proporciona una implementación de alto nivel para subprocesos múltiples y es el estándar actual.
- Para crear un hilo usando el módulo de enhebrado, debe hacer lo siguiente:
- Cree una clase que amplíe la clase Thread .
- Anula su constructor (__init__).
- Anula su método run () .
- Crea un objeto de esta clase.
- Un hilo se puede ejecutar llamando al método start () .
- El método join () se puede usar para bloquear otros hilos hasta que este hilo (en el que se llamó a join) finalice la ejecución.
- Se produce una condición de carrera cuando varios subprocesos acceden o modifican un recurso compartido al mismo tiempo.
- Puede evitarse sincronizando subprocesos.
- Python admite 6 formas de sincronizar subprocesos:
- Cerraduras
- RCerraduras
- Semáforos
- Condiciones
- Eventos y
- Barreras
- Los candados permiten que solo un subproceso en particular que haya adquirido el candado entre en 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.
- release () : establece el estado de bloqueo en desbloqueado y vuelve. Si se llama a un objeto desbloqueado, devuelve falso.
- El bloqueo de intérprete global es un mecanismo mediante el cual solo se puede ejecutar 1 proceso de intérprete de CPython a la vez.
- Se utilizó para facilitar la funcionalidad de recuento de referencias del recolector de basura de CPythons.
- Para crear aplicaciones de Python con operaciones intensas vinculadas a la CPU, debe usar el módulo de multiprocesamiento.