Python3 Multihilo
El multihilo es similar a ejecutar múltiples programas diferentes al mismo tiempo. La ejecución con hilos tiene las siguientes ventajas:
- El uso de hilos permite poner en segundo plano las tareas que ocupan mucho tiempo en un programa.
- La interfaz de usuario puede ser más atractiva; por ejemplo, cuando el usuario hace clic en un botón para activar el procesamiento de ciertos eventos, puede aparecer una barra de progreso que muestre el avance del procesamiento.
- La velocidad de ejecución del programa puede aumentar.
- En la implementación de algunas tareas de espera, como la entrada del usuario, la lectura/escritura de archivos y el envío/recepción de datos por red, los hilos son bastante útiles. En estos casos, podemos liberar algunos recursos valiosos como el uso de memoria, etc.
Cada hilo independiente tiene una entrada de ejecución, una secuencia de ejecución ordenada y una salida del programa. Sin embargo, los hilos no pueden ejecutarse de manera independiente; deben existir dentro de una aplicación, y la aplicación proporciona el control de ejecución de múltiples hilos.
Cada hilo tiene su propio conjunto de registros de CPU, llamado contexto del hilo. Este contexto refleja el estado de los registros de CPU de la última vez que el hilo se ejecutó.
El puntero de instrucción y el puntero de pila son los dos registros más importantes del contexto de un hilo. Los hilos siempre se ejecutan dentro del contexto del proceso; estas direcciones se utilizan para marcar la memoria en el espacio de direcciones del proceso que posee el hilo.
- Los hilos pueden ser expropiados (interrumpidos).
- Mientras otros hilos se están ejecutando, un hilo puede suspenderse temporalmente (también llamado dormir) — esto es la cesión del hilo.
Los hilos se pueden clasificar en:
- Hilos de kernel:Creados y cancelados por el núcleo del sistema operativo.
- Hilos de usuario:Hilos implementados en programas de usuario sin necesidad de soporte del kernel.
Los dos módulos comúnmente utilizados para hilos en Python3 son:
- _thread
- threading (recomendado)
El módulo thread ha quedado obsoleto. Los usuarios pueden usar el módulo threading en su lugar. Por lo tanto, en Python3 ya no se puede usar el módulo "thread". Por compatibilidad, Python3 renombró thread como "_thread".
Comenzando con los hilos de Python
En Python hay dos formas de usar hilos: mediante funciones o usando clases para envolver objetos de hilo.
Forma funcional: llamar a la función start_new_thread() del módulo _thread para crear un nuevo hilo. La sintaxis es la siguiente:
_thread.start_new_thread ( function, args[, kwargs] )
Descripción de parámetros:
- function - la función del hilo.
- args - los argumentos pasados a la función del hilo; deben ser de tipo tupla.
- kwargs - parámetros opcionales.
Ejemplo
import _thread
import time
# define una función para el hilo
def print_time( threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print ("%s: %s" % ( threadName, time.ctime(time.time()) ))
# crea dos hilos
try:
_thread.start_new_thread( print_time, ("Thread-1", 2, ) )
_thread.start_new_thread( print_time, ("Thread-2", 4, ) )
except:
print ("Error: no se puede iniciar el hilo")
while 1:
pass
La salida del programa anterior es la siguiente:
Thread-1: Wed Jan 5 17:38:08 2022 Thread-2: Wed Jan 5 17:38:10 2022 Thread-1: Wed Jan 5 17:38:10 2022 Thread-1: Wed Jan 5 17:38:12 2022 Thread-2: Wed Jan 5 17:38:14 2022 Thread-1: Wed Jan 5 17:38:14 2022 Thread-1: Wed Jan 5 17:38:16 2022 Thread-2: Wed Jan 5 17:38:18 2022 Thread-2: Wed Jan 5 17:38:22 2022 Thread-2: Wed Jan 5 17:38:26 2022
Después de ejecutar el programa anterior, puedes presionar ctrl-c para salir.
Módulo de hilos
Python3 proporciona soporte para hilos a través de dos bibliotecas estándar: _thread y threading.
_thread proporciona hilos de bajo nivel y primitivos, así como un bloqueo simple. Sus funcionalidades son bastante limitadas en comparación con el módulo threading.
Además de incluir todos los métodos del módulo _thread, el módulo threading también proporciona otros métodos:
- threading.current_thread(): devuelve la variable actual del hilo.
- threading.enumerate(): devuelve una lista de hilos en ejecución. "En ejecución" significa después de que el hilo ha sido iniciado y antes de que termine, sin incluir los hilos antes de iniciarse ni después de terminar.
- threading.active_count(): devuelve el número de hilos en ejecución, con el mismo resultado que len(threading.enumerate()).
- threading.Thread(target, args=(), kwargs={}, daemon=None):
- Crear
Threaduna instancia de la clase. target: la función objetivo que el hilo ejecutará.args: los argumentos de la función objetivo, pasados en forma de tupla.kwargs: los argumentos de palabra clave de la función objetivo, pasados en forma de diccionario.daemon: especifica si el hilo es un hilo daemon.
- Crear
La clase threading.Thread proporciona los siguientes métodos y atributos:
__init__(self, group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None):- Inicializa
Threadel objeto. group: grupo de hilos, aún no utilizado, reservado para futuras extensiones.target: la función objetivo que el hilo ejecutará.name: el nombre del hilo.args: los argumentos de la función objetivo, pasados en forma de tupla.kwargs: los argumentos de palabra clave de la función objetivo, pasados en forma de diccionario.daemon: especifica si el hilo es un hilo daemon.
- Inicializa
start(self):- Inicia el hilo. Se llamará al
run()método.
- Inicia el hilo. Se llamará al
run(self):- En este método, el hilo define el código que se ejecutará.
join(self, timeout=None):- Espera a que el hilo termine. Por defecto,
join()se bloqueará hasta que el hilo llamado termine. Si se especifica eltimeoutparámetro timeout, esperará como máximotimeoutsegundos.
- Espera a que el hilo termine. Por defecto,
is_alive(self):- Devuelve si el hilo está en ejecución. Si el hilo ya se ha iniciado y aún no ha terminado, devuelve
True, de lo contrario devuelveFalse。
- Devuelve si el hilo está en ejecución. Si el hilo ya se ha iniciado y aún no ha terminado, devuelve
getName(self):- Devuelve el nombre del hilo.
setName(self, name):- Establece el nombre del hilo.
identAtributo:- El identificador único del hilo.
daemonAtributo:- El indicador daemon del hilo, utilizado para indicar si es un hilo daemon.
isDaemon()Método:
Un ejemplo sencillo de hilo:
Ejemplo
import time
def print_numbers():
for i in range(5):
time.sleep(1)
print(i)
# crea un hilo
thread = threading.Thread(target=print_numbers)
# inicia el hilo
thread.start()
# espera a que el hilo termine
thread.join()
El resultado de la salida es:
0 1 2 3 4
Crear hilos con el módulo threading
Podemos crear una nueva subclase heredando directamente de threading.Thread, y tras instanciarla llamar al método start() para iniciar el nuevo hilo; es decir, se llama al método run() del hilo:
Ejemplo
import threading
import time
exitFlag = 0
class myThread (threading.Thread):
def __init__(self, threadID, name, delay):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.delay = delay
def run(self):
print ("Iniciando hilo:" + self.name)
print_time(self.name, self.delay, 5)
print ("Saliendo del hilo:" + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
threadName.exit()
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
# crea un nuevo hilo
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# inicia el nuevo hilo
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("Saliendo del hilo principal")
El resultado de ejecutar el programa anterior es el siguiente:
开始线程:Thread-1 开始线程:Thread-2 Thread-1: Wed Jan 5 17:34:54 2022 Thread-2: Wed Jan 5 17:34:55 2022 Thread-1: Wed Jan 5 17:34:55 2022 Thread-1: Wed Jan 5 17:34:56 2022 Thread-2: Wed Jan 5 17:34:57 2022 Thread-1: Wed Jan 5 17:34:57 2022 Thread-1: Wed Jan 5 17:34:58 2022 退出线程:Thread-1 Thread-2: Wed Jan 5 17:34:59 2022 Thread-2: Wed Jan 5 17:35:01 2022 Thread-2: Wed Jan 5 17:35:03 2022 退出线程:Thread-2 退出主线程
Sincronización de hilos
Si varios hilos modifican conjuntamente algunos datos, pueden aparecer resultados impredecibles. Para garantizar la corrección de los datos, es necesario sincronizar los múltiples hilos.
Usando Lock y RLock de los objetos Thread se puede implementar una sincronización simple de hilos. Estos dos objetos tienen métodos acquire y release. Para aquellos datos que solo permitan que un hilo los opere a la vez, se pueden colocar sus operaciones entre los métodos acquire y release. Así:
La ventaja del multihilo es que puede ejecutar múltiples tareas al mismo tiempo (al menos así parece). Pero cuando los hilos necesitan compartir datos, puede existir el problema de la desincronización de datos.
Considere la siguiente situación: todos los elementos de una lista son 0; el hilo "set" cambia todos los elementos de atrás hacia adelante a 1, y el hilo "print" se encarga de leer la lista de adelante hacia atrás e imprimirla.
Entonces, cuando el hilo "set" comienza a modificar, el hilo "print" puede venir a imprimir la lista, y la salida queda mitad 0 y mitad 1; esto es la desincronización de datos. Para evitar esta situación, se introduce el concepto de bloqueo.
Un bloqueo tiene dos estados: bloqueado y desbloqueado. Cada vez que un hilo, como "set", quiere acceder a datos compartidos, primero debe obtener el bloqueo; si otro hilo, como "print", ya ha obtenido el bloqueo, entonces se hace que el hilo "set" se pause, es decir, se bloquea la sincronización; cuando el hilo "print" termina de acceder y libera el bloqueo, entonces se permite que el hilo "set" continúe.
Con este procesamiento, al imprimir la lista se imprime todo 0 o todo 1, y ya no aparecerá la incómoda situación de mitad 0 y mitad 1.
Ejemplo
import threading
import time
class myThread (threading.Thread):
def __init__(self, threadID, name, delay):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.delay = delay
def run(self):
print ("Iniciando hilo: " + self.name)
# obtiene el bloqueo para la sincronización de hilos
threadLock.acquire()
print_time(self.name, self.delay, 3)
# libera el bloqueo, inicia el siguiente hilo
threadLock.release()
def print_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
threadLock = threading.Lock()
threads = []
# crea un nuevo hilo
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# inicia el nuevo hilo
thread1.start()
thread2.start()
# añade el hilo a la lista de hilos
threads.append(thread1)
threads.append(thread2)
# espera a que todos los hilos terminen
for t in threads:
t.join()
print ("Saliendo del hilo principal")
Al ejecutar el programa anterior, el resultado de salida es:
开启线程: Thread-1 开启线程: Thread-2 Thread-1: Wed Jan 5 17:36:50 2022 Thread-1: Wed Jan 5 17:36:51 2022 Thread-1: Wed Jan 5 17:36:52 2022 Thread-2: Wed Jan 5 17:36:54 2022 Thread-2: Wed Jan 5 17:36:56 2022 Thread-2: Wed Jan 5 17:36:58 2022 退出主线程
Cola de prioridad de hilos (Queue)
El módulo Queue de Python proporciona clases de cola sincronizadas y seguras para hilos, incluyendo la cola FIFO (primero en entrar, primero en salir) Queue, la cola LIFO (último en entrar, primero en salir) LifoQueue, y la cola de prioridad PriorityQueue.
Estas colas implementan primitivas de bloqueo y pueden usarse directamente en múltiples hilos; se pueden usar colas para lograr la sincronización entre hilos.
Métodos comunes en el módulo Queue:
- Queue.qsize() devuelve el tamaño de la cola.
- Queue.empty() Si la cola está vacía, devuelve True; de lo contrario, False.
- Queue.full() Si la cola está llena, devuelve True; de lo contrario, False.
- Queue.full se corresponde con el tamaño de maxsize.
- Queue.get([block[, timeout]]) obtiene un elemento de la cola, timeout es el tiempo de espera.
- Queue.get_nowait() equivale a Queue.get(False).
- Queue.put(item) escribe en la cola, timeout es el tiempo de espera.
- Queue.put_nowait(item) equivale a Queue.put(item, False).
- Queue.task_done() Después de completar una tarea, la función Queue.task_done() envía una señal a la cola indicando que la tarea ha sido completada.
- Queue.join() en realidad significa esperar a que la cola esté vacía antes de realizar otras operaciones.
Ejemplo
import queue
import threading
import time
exitFlag = 0
class myThread (threading.Thread):
def __init__(self, threadID, name, q):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.q = q
def run(self):
print (Iniciando hilo: + self.name)
process_data(self.name, self.q)
print (Saliendo del hilo: + self.name)
def process_data(threadName, q):
while not exitFlag:
queueLock.acquire()
if not workQueue.empty():
data = q.get()
queueLock.release()
print ("%s processing %s" % (threadName, data))
else:
queueLock.release()
time.sleep(1)
threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1
# Crear un nuevo hilo
for tName in threadList:
thread = myThread(threadID, tName, workQueue)
thread.start()
threads.append(thread)
threadID += 1
# Llenar la cola
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()
# Esperar a que la cola se vacíe
while not workQueue.empty():
pass
# Notificar a los hilos que es hora de salir
exitFlag = 1
# Esperar a que todos los hilos terminen
for t in threads:
t.join()
print (Saliendo del hilo principal)
Resultado de la ejecución del programa anterior:
开启线程:Thread-1 开启线程:Thread-2 开启线程:Thread-3 Thread-3 processing One Thread-1 processing Two Thread-2 processing Three Thread-3 processing Four Thread-1 processing Five 退出线程:Thread-3 退出线程:Thread-2 退出线程:Thread-1 退出主线程Otras extensiones