Curso Python. Volumen XX: Hilos (Threading). Parte II

Escrito por Javier Ceballos Fernández
Programación
0

Bienvenidos un día más al curso de Python. En este capítulo vamos a continuar con la ejecución de hilos, para poder realizar tareas simultáneas en nuestras aplicaciones. Para ello, vamos a mostraros cómo podemos hacer que los hilos se ejecuten por un tiempo determinado, y cómo podemos finalizar su ejecución de una manera correcta. Así que pongámonos manos a la obra.

Hilos que funcionan durante un tiempo

En ocasiones nos puede interesar que los hilos se ejecuten por un tiempo determinado. En el ejemplo que os vamos a mostrar a continuación, hemos iniciado 5 hilos que funcionarán durante un segundo. La tarea de cada hilo consiste en incrementar un contador hasta que se alcance el tiempo límite de ejecución. Se ha utilizado el módulo “time” para obtener el momento inicial y calcular el tiempo límite de ejecución.

Cuando finaliza el tiempo de cada hilo, el valor máximo contado se va añadiendo a un diccionario que se muestra cuando el último hilo activo está finalizando.

Para conocer cuándo está finalizando el último hilo, utilizaremos la función “threading.active_count()” la cual nos devuelve el número de hilos que aún quedan activos, incluyendo el hilo principal (que se corresponde con el hilo que inicia el propio programa), es decir, cuando el último hilo “Thread” esté terminando quedarán activos 2 hilos.

Por último se mostrará al final una lista con la información de estos hilos, esta información ha sido obtenida a través de la función “threading.enumerate()”.

Comentaros también que la variable “vmax_hilos” contiene los valores máximos del contador de cada hilo. Esta variable se inicializa al comienzo del programa y se declara después como global dentro de la función. Esto se hace para lograr mantener “vivos” los valores máximos que se añaden al diccionario al concluir cada hilo. Si no se declara como global sólo permanecerá el último valor agregado.

import threading, time

vmax_hilos = {}

def contar(segundos):
    """Contar hasta un límite de tiempo"""
    global vmax_hilos
    contador = 0
    inicial = time.time()
    limite = inicial + segundos
    nombre = threading.current_thread().getName()
    while inicial <= limite:
        contador += 1
        inicial = time.time()
        print(nombre, contador)

    vmax_hilos[nombre] = contador
    if threading.active_count() == 2:
        print(vmax_hilos)
        print(threading.enumerate())


segundos = 1
for num_hilo in range(5):
    hilo = threading.Thread(name='hilo%s' % num_hilo,
                            target=contar,
                            args=(segundos,))
    hilo.start()

Demonios

Tenemos dos métodos diferentes para finalizar un programa basado en hilos correctamente. En el primer método, el hilo principal del programa espera a que todos los hilos creados con “Thread” terminen su trabajo. Este es el caso de todos los ejemplos mostrados hasta el momento.

En el segundo método, el hilo principal del programa puede finalizar aunque uno o más hilos hijos no hayan terminado su tarea. Teniendo en cuenta que cuando finalice el hilo principal también lo harán estos hilos especiales llamados “demonios”. Si existen hilos no-demonios el hilo principal esperará a que éstos concluyan su trabajo. Los demonios son útiles para programas que realizan operaciones de monitorización o de chequeo de recursos, servicios, aplicaciones, etc.

Para declarar un hilo como demonio se asigna “True” al argumento “daemon” al crear el objeto “Thread”, o bien, se establece dicho valor con posterioridad con el método “set_daemon()”.

El ejemplo que os mostramos a continuación, utiliza dos hilos: un hilo estará escribiendo en un archivo y mientras el otro hilo (el demonio) estará comprobando el tamaño del archivo cada cierto tiempo. Cuando el hilo encargado de escribir termina, todo el programa llega a su fin a pesar de que el contador del demonio no ha alcanzado el valor límite.

import time, os, threading

def comprobar(nombre):
    '''Comprueba el tamaño de archivo'''
    contador = 0
    tam = 0
    while contador <= 99:
        contador += 1
        if os.path.exists(nombre):
            estado = os.stat(nombre)
            tam = estado.st_size

        print(threading.current_thread().getName(),
              contador,
              tam,
              'bytes')

        time.sleep(0.1)

def escribir(nombre):
    '''Escribe en archivo'''
    contador = 1
    while contador <= 10:
        with open(nombre, 'a') as archivo:
            archivo.write('1')
            print(threading.current_thread().getName(),
                  contador)
            time.sleep(0.3)
            contador += 1

nombre = 'archivo.txt'
if os.path.exists(nombre):
    os.remove(nombre)

hilo1 = threading.Thread(name='comprobar',
                         target=comprobar,
                         args=(nombre,),
                         daemon=True)

hilo2 = threading.Thread(name='escribir',
                         target=escribir,
                         args=(nombre,))
hilo1.start()
hilo2.start()

Para hacer que el hilo principal espere a que el hilo demonio complete su trabajo, utilizaremos el método “join()” con dicho hilo. El método “isAlive()” también es útil para conocer si un hilo está o no activo. En el ejemplo retorna “False” porque el demonio ya ha terminado:

import time, os, threading

def comprobar(nombre):
    '''Comprueba tamaño de archivo'''
    contador = 0
    tam = 0
    while contador < 100:
        contador += 1
        if os.path.exists(nombre):
            estado = os.stat(nombre)
            tam = estado.st_size

        print(threading.current_thread().getName(),
              contador,
              tam,
              'bytes')

        time.sleep(0.1)

def escribir(nombre):
    '''Escribe en archivo'''
    contador = 1
    while contador <= 10:
        with open(nombre, 'a') as archivo:
            archivo.write('1')
            print(threading.current_thread().getName(),
                  contador)
            time.sleep(0.3)
            contador += 1


nombre = 'archivo.txt'
if os.path.exists(nombre):
    os.remove(nombre)

hilo1 = threading.Thread(name='comprobar',
                         target=comprobar,
                         args=(nombre,),
                         daemon=True)

hilo2 = threading.Thread(name='escribir',
                         target=escribir,
                         args=(nombre,))
hilo1.start()
hilo2.start()

hilo1.join()
print(hilo1.isAlive())

Controlar la ejecución de varios demonios

Cuando un programa utiliza un gran número de demonios y se quiere que el hilo principal espere a que todos los demonios terminen su ejecución, utilizaremos el método “join()” con cada demonio. Para hacer el seguimiento de los hilos activos se puede emplear “enumerate()” pero teniendo en cuenta que dentro de la lista que devuelve se incluye el hilo principal. Con este hilo hay que tener cuidado porque no acepta ciertas operaciones: no se puede obtener su nombre con “getName()” o utilizar el método “join()”.

En el ejemplo que mostramos a continuación, se utiliza la función “threading.main_thread()” para identificar al hilo principal. Después, se recorren todos los hilos activos para ejecutar “join()”, excluyendo al principal.

import threading

def contar(numero):
    contador = 0
    while contador<10:
        contador+=1
        print(numero, threading.get_ident(), contador)

for numero in range(1, 11):
    hilo = threading.Thread(target=contar,
                            args=(numero,),
                            daemon=True)
    hilo.start()

# Obtiene hilo principal

hilo_ppal = threading.main_thread()

# Recorre hilos activos para controlar estado de su ejecución

for hilo in threading.enumerate():

    # Si el hilo es hilo_ppal continua al siguiente hilo activo

    if hilo is hilo_ppal:
        continue

    # Se obtiene información hilo actual y núm. hilos activos

    print(hilo.getName(),
          hilo.ident,
          hilo.isDaemon(),
          threading.active_count())

    # El programa esperará a que este hilo finalice:

    hilo.join()

Crear una subclase “Thread” y redefinir sus métodos

Cuando empieza la ejecución de un hilo se invoca de manera automática al método subyacente “run()”, que es el que llama a la función que pasamos al constructor. Para poder crear una subclase “Thread” es necesario reescribir como mínimo el método “run()” con la nueva funcionalidad.

import threading

class MiHilo(threading.Thread):
    def run(self):
        contador = 1
        while contador <= 10:
            print('ejecutando',
                  threading.current_thread().getName(),
                  contador)
            contador+=1

for numero in range(10):
    hilo = MiHilo()
    hilo.start()

Si se quiere pasar valores utilizando los argumentos “args” y/o “kwargs” será necesario reescribir el método “__init__()”. Por defecto, el constructor “Thread” utiliza variables privadas para estos argumentos. En el siguiente ejemplo se declara una subclase con dos argumentos.

import threading
class MiHilo(threading.Thread):
    def __init__(self, group=None, target=None, name=None,
                 args=(), kwargs=None, *, daemon=None):
        super().__init__(group=group, target=target, name=name,
                         daemon=daemon)
        self.arg1 = args[0]
        self.arg2 = args[1]

    def run(self):
        contador = 1
        while contador <= 10:
            print('ejecutando...',
                  'contador', contador,
                  'argumento1', self.arg1,
                  'argumento2', self.arg2)
            contador+=1

for numero in range(10):
    hilo = MiHilo(args=(numero,numero*numero), daemon=False)
    hilo.start()

Aquí lo dejamos por hoy, para que podáis ir asimilando los nuevos conceptos que os hemos explicado, os invitamos como siempre a que vayáis probando todo lo aprendido hasta el momento.

Y para todos los que se acaban de incorporar indicarles que tenemos un índice con todos los capítulos del curso, ya que nunca es tarde para empezar.


Últimos análisis

Valoración RZ
9
Valoración RZ
10
Valoración RZ
8
Valoración RZ
10
Valoración RZ
9
Valoración RZ
9
Valoración RZ
10
Valoración RZ
8
Valoración RZ
10