Desarrollo de subprocesos de Python: una introducción – kirukiru.es

En este tutorial, aprenderá a usar el módulo de subprocesos integrado de Python para explorar las capacidades de subprocesos múltiples en Python.

Comenzando con los conceptos básicos de procesos y subprocesos, aprenderá cómo funcionan los subprocesos múltiples en Python, mientras comprende los conceptos de concurrencia y paralelismo. Luego, aprenderá a iniciar y ejecutar uno o más subprocesos en Python utilizando el módulo de subprocesos integrado.

Empecemos.

Procesos versus subprocesos: ¿cuáles son las diferencias?

¿Qué es un proceso?

Un proceso es cualquier instancia de un programa que necesita ejecutarse.

Puede ser cualquier cosa: una secuencia de comandos de Python o un navegador web como Chrome para una aplicación de videoconferencia. Si inicia el Administrador de tareas en su máquina y navega hasta Rendimiento -> CPU, podrá ver los procesos y subprocesos que se están ejecutando actualmente en los núcleos de su CPU.

Comprensión de procesos y subprocesos

Internamente, un proceso tiene una memoria dedicada que almacena el código y los datos correspondientes al proceso.

Un proceso consta de uno o más subprocesos. Un hilo es la secuencia más pequeña de instrucciones que el sistema operativo puede ejecutar y representa el flujo de ejecución.

Cada subproceso tiene su propia pila y registros, pero no una memoria dedicada. Todos los subprocesos asociados con un proceso pueden acceder a los datos. Por lo tanto, los datos y la memoria son compartidos por todos los subprocesos de un proceso.

En una CPU con N núcleos, N procesos pueden ejecutarse en paralelo en la misma instancia de tiempo. Sin embargo, dos subprocesos del mismo proceso nunca pueden ejecutarse en paralelo, pero pueden ejecutarse simultáneamente. Abordaremos el concepto de concurrencia frente a paralelismo en la siguiente sección.

Basándonos en lo que hemos aprendido hasta ahora, resumamos las diferencias entre un proceso y un hilo.

CaracterísticaProcesoSubprocesoMemoriaMemoria dedicadaMemoria compartidaModo de ejecuciónParalelo, concurrenteConcurrente; pero no ejecución paralela manejada por el sistema operativo CPython Interpreter

Multihilo en Python

En Python, el bloqueo de intérprete global (GIL) garantiza que solo un subproceso pueda adquirir el bloqueo y ejecutarse en cualquier momento. Todos los subprocesos deben adquirir este bloqueo para ejecutarse. Esto garantiza que solo un subproceso único pueda estar en ejecución, en un momento dado, y evita subprocesos múltiples simultáneos.

Por ejemplo, considere dos subprocesos, t1 y t2, del mismo proceso. Debido a que los subprocesos comparten los mismos datos cuando t1 lee un valor k particular, t2 puede modificar el mismo valor k. Esto puede provocar interbloqueos y resultados no deseados. Pero solo uno de los subprocesos puede adquirir el bloqueo y ejecutarse en cualquier instancia. Por lo tanto, GIL también garantiza la seguridad de los hilos.

Entonces, ¿cómo logramos capacidades de subprocesos múltiples en Python? Para entender esto, analicemos los conceptos de concurrencia y paralelismo.

Concurrencia frente a paralelismo: una descripción general

Considere una CPU con más de un núcleo. En la siguiente ilustración, la CPU tiene cuatro núcleos. Esto significa que podemos tener cuatro operaciones diferentes ejecutándose en paralelo en un instante dado.

Si hay cuatro procesos, cada uno de los procesos puede ejecutarse de forma independiente y simultánea en cada uno de los cuatro núcleos. Supongamos que cada proceso tiene dos subprocesos.

Para comprender cómo funciona el subprocesamiento, pasemos de una arquitectura de procesador multinúcleo a una de un solo núcleo. Como se mencionó, solo un único subproceso puede estar activo en una instancia de ejecución en particular; pero el núcleo del procesador puede cambiar entre los subprocesos.

Por ejemplo, los subprocesos vinculados a E/S a menudo esperan operaciones de E/S: lectura de entrada del usuario, lecturas de bases de datos y operaciones de archivos. Durante este tiempo de espera, puede liberar el bloqueo para que el otro subproceso pueda ejecutarse. El tiempo de espera también puede ser una operación simple, como dormir durante n segundos.

En resumen: durante las operaciones de espera, el subproceso libera el bloqueo, lo que permite que el núcleo del procesador cambie a otro subproceso. El subproceso anterior reanuda la ejecución después de que finaliza el período de espera. Este proceso, en el que el núcleo del procesador cambia entre los subprocesos al mismo tiempo, facilita la multiproceso. ✅

Si desea implementar el paralelismo a nivel de proceso en su aplicación, considere usar multiprocesamiento en su lugar.

Módulo de subprocesamiento de Python: Primeros pasos

Python se envía con un módulo de subprocesamiento que puede importar al script de Python.

import threading

Para crear un objeto de hilo en Python, puede usar el constructor de hilos: threading.Thread(…). Esta es la sintaxis genérica que es suficiente para la mayoría de las implementaciones de subprocesos:

threading.Thread(target=...,args=...)

Aquí,

  • target es el argumento de palabra clave que denota un Python invocable
  • args es la tupla de argumentos que recibe el destino.

Necesitará Python 3.x para ejecutar los ejemplos de código de este tutorial. Descarga el código y síguelo.

Cómo definir y ejecutar subprocesos en Python

Definamos un hilo que ejecuta una función de destino.

La función de destino es some_func.

import threading
import time

def some_func():
    print("Running some_func...")
    time.sleep(2)
    print("Finished running some_func.")

thread1 = threading.Thread(target=some_func)
thread1.start()
print(threading.active_count())

Analicemos lo que hace el fragmento de código anterior:

  • Importa los hilos y los módulos de tiempo.
  • La función some_func tiene instrucciones print() descriptivas e incluye una operación de suspensión durante dos segundos: time.sleep(n) hace que la función entre en suspensión durante n segundos.
  • A continuación, definimos un subproceso thread_1 con el objetivo como some_func. threading.Thread(target=…) crea un objeto de hilo.
  • Nota: Especifique el nombre de la función y no una llamada de función; use some_func y no some_func().
  • La creación de un objeto de hilo no inicia un hilo; llamar al método start () en el objeto de hilo sí lo hace.
  • Para obtener el número de subprocesos activos, usamos la función active_count().

La secuencia de comandos de Python se ejecuta en el subproceso principal y estamos creando otro subproceso (subproceso1) para ejecutar la función some_func, por lo que el recuento de subprocesos activos es dos, como se ve en el resultado:

# Output
Running some_func...
2
Finished running some_func.

Si echamos un vistazo más de cerca a la salida, vemos que al iniciar thread1, se ejecuta la primera declaración de impresión. Pero durante la operación de suspensión, el procesador cambia al subproceso principal e imprime la cantidad de subprocesos activos, sin esperar a que subproceso1 termine de ejecutarse.

Esperando a que los subprocesos finalicen la ejecución

Si desea que el subproceso1 finalice la ejecución, puede llamar al método join() después de iniciar el subproceso. Si lo hace, esperará a que subproceso1 termine de ejecutarse sin cambiar al subproceso principal.

import threading
import time

def some_func():
    print("Running some_func...")
    time.sleep(2)
    print("Finished running some_func.")

thread1 = threading.Thread(target=some_func)
thread1.start()
thread1.join()
print(threading.active_count())

Ahora, thread1 ha terminado de ejecutarse antes de que imprimamos el recuento de subprocesos activos. Entonces, solo se está ejecutando el subproceso principal, lo que significa que el recuento de subprocesos activos es uno. ✅

# Output
Running some_func...
Finished running some_func.
1

Cómo ejecutar varios subprocesos en Python

A continuación, creemos dos subprocesos para ejecutar dos funciones diferentes.

Aquí, count_down es una función que toma un número como argumento y cuenta hacia atrás desde ese número hasta cero.

def count_down(n):
    for i in range(n,-1,-1):
        print(i)

Definimos count_up, otra función de Python que cuenta desde cero hasta un número dado.

def count_up(n):
    for i in range(n+1):
        print(i)

📑 Cuando se usa la función range() con la sintaxis range(start, stop, step), la parada del punto final se excluye de forma predeterminada.

– Para contar hacia atrás desde un número específico hasta cero, puede usar un valor de paso negativo de -1 y establecer el valor de parada en -1 para que se incluya el cero.

– De manera similar, para contar hasta n, debe establecer el valor de parada en n + 1. Debido a que los valores predeterminados de inicio y paso son 0 y 1, respectivamente, puede usar el rango (n + 1) para obtener la secuencia 0 a través de n.

A continuación, definimos dos subprocesos, subproceso1 y subproceso2 para ejecutar las funciones count_down y count_up, respectivamente. Agregamos declaraciones de impresión y operaciones de suspensión para ambas funciones.

Al crear los objetos de subproceso, tenga en cuenta que los argumentos de la función de destino deben especificarse como una tupla, en el parámetro args. Como ambas funciones (count_down y count_up) toman un argumento. Deberá insertar una coma explícitamente después del valor. Esto garantiza que el argumento se siga pasando como una tupla, ya que los elementos subsiguientes se infieren como Ninguno.

import threading
import time

def count_down(n):
    for i in range(n,-1,-1):
        print("Running thread1....")
        print(i)
        time.sleep(1)


def count_up(n):
    for i in range(n+1):
        print("Running thread2...")
        print(i)
        time.sleep(1)

thread1 = threading.Thread(target=count_down,args=(10,))
thread2 = threading.Thread(target=count_up,args=(5,))
thread1.start()
thread2.start()

En la salida:

  • La función count_up se ejecuta en thread2 y cuenta hasta 5 comenzando en 0.
  • La función count_down se ejecuta en thread1 y cuenta regresivamente de 10 a 0.
# Output
Running thread1....
10
Running thread2...
0
Running thread1....
9
Running thread2...
1
Running thread1....
8
Running thread2...
2
Running thread1....
7
Running thread2...
3
Running thread1....
6
Running thread2...
4
Running thread1....
5
Running thread2...
5
Running thread1....
4
Running thread1....
3
Running thread1....
2
Running thread1....
1
Running thread1....
0

Puede ver que thread1 y thread2 se ejecutan alternativamente, ya que ambos implican una operación de espera (suspensión). Una vez que la función count_up ha terminado de contar hasta 5, thread2 ya no está activo. Entonces obtenemos la salida correspondiente solo a thread1.

Resumiendo

En este tutorial, ha aprendido a usar el módulo de subprocesos integrado de Python para implementar subprocesos múltiples. He aquí un resumen de los puntos clave:

  • El constructor de subprocesos se puede utilizar para crear un objeto de subproceso. El uso de threading.Thread(target=,args=()) crea un subproceso que ejecuta el objetivo invocable con argumentos especificados en args.
  • El programa Python se ejecuta en un subproceso principal, por lo que los objetos de subproceso que crea son subprocesos adicionales. Puede llamar a la función active_count() para devolver el número de subprocesos activos en cualquier instancia.
  • Puede iniciar un subproceso con el método start() en el objeto del subproceso y esperar hasta que finalice la ejecución con el método join().

Puede codificar ejemplos adicionales modificando los tiempos de espera, probando una operación de E/S diferente y más. Asegúrese de implementar subprocesos múltiples en sus próximos proyectos de Python. ¡Feliz codificación!🎉