Ejecutar un código cada determinado tiempo

Ejecutar un código cada determinado tiempo

En este artículo veremos cómo programar una función para que se ejecute en segundo plano cada un tiempo determinado. En otros lenguajes esto es habitualmente implementado a través de una clase Timer. Python incluye en su librería estándar la clase threading.Timer, pero solo permite ejecutar un código por única vez pasada una cantidad de segundos.

import threading

def f():
    print("¡Hola, mundo!")

# Ejecutar la función luego de 3 segundos.
t = threading.Timer(3, f)
t.start()
print("Esto se ejecuta antes que la función f().")

La clase threading.Timer mueve la ejecución de la función a un hilo secundario, por lo cual t.start() no bloquea la ejecución del código y el print() final se ejecuta antes que la función f().

Bajo este mismo principio podemos emplear la clase threading.Thread para mover a otro hilo de ejecución una función en cuyo interior se repita la ejecución de un código cada un determinado tiempo, con el auxilio de la función estándar time.sleep().

import threading
import time

# Tarea a ejecutarse cada determinado tiempo.
def timer():
    while True:
        print("¡Hola, mundo!")
        time.sleep(3)   # 3 segundos.

# Iniciar la ejecución en segundo plano.
t = threading.Thread(target=timer)
t.start()

La lógica es simple: dentro de la función usamos un bucle infinito, pero en cada iteración pausamos el código durante 3 segundos (o el tiempo que se desee). La pausa no afecta la ejecución del programa porque la función corre en un hilo secundario.

Ahora bien, ¿qué sucede si en otro momento queremos detener la ejecución del timer? Los hilos de Python no incluyen un método para detener su ejecución. Necesitamos, entonces, una forma de comunicarle a timer() que debe detener la ejecución del bucle para que la función termine. Una solución básica para comunicar dos hilos de ejecución es la clase threading.Event que, a grandes rasgos, es una forma segura de compartir un booleano entre dos hilos. Así, es posible crear un booleano que indique si el timer debe ejecutarse o no y susceptible de ser leído y modificado tanto desde el hilo principal como desde el interior de la función timer().

import threading
import time

def timer(timer_runs):
    # (4) El código corre mientras el booleano sea verdadero.
    while timer_runs.is_set():
        print("¡Hola, mundo!")
        time.sleep(3)   # 3 segundos.

# (1) Creación del booleano que indica si el hilo secundario
# debe correr o no.
timer_runs = threading.Event()
# (2) Iniciarlo con el valor True.
timer_runs.set()
# (3) Pasarlo como argumento al timer para que pueda leerlo.
t = threading.Thread(target=timer, args=(timer_runs,))
t.start()

En algún otro momento del código, cuando se desea detener la ejecución del timer, simplemente llámese al método clear() para cambiar a False el valor del booleano.

# Detener la ejecución del timer.
timer_runs.clear()

Por ejemplo, el siguiente código lanza el timer y espera 10 segundos para detenerlo.

import threading
import time

def timer(timer_runs):
    while timer_runs.is_set():
        print("¡Hola, mundo!")
        time.sleep(3)   # 3 segundos.

timer_runs = threading.Event()
timer_runs.set()
t = threading.Thread(target=timer, args=(timer_runs,))
t.start()
# Esperar 10 segundos y luego detener el timer.
time.sleep(10)
timer_runs.clear()
print("¡El timer ha sido detenido!")

El resultado en pantalla es:

¡Hola, mundo!
¡Hola, mundo!
¡Hola, mundo!
¡Hola, mundo!
¡El timer ha sido detenido!

Esta lógica puede abstraerse en una clase padre Timer que se ocupe de todos los detalles relativos al bucle y a la pausa del código, y así luego poder crear múltiples timers con menos código y una API más elegante:

class Timer(threading.Thread):

    def __init__(self):
        self._timer_runs = threading.Event()
        self._timer_runs.set()
        super().__init__()

    def run(self):
        while self._timer_runs.is_set():
            self.timer()
            time.sleep(self.__class__.interval)
    
    def stop(self):
        self._timer_runs.clear()

Ahora definir un timer no es más que esto:

class HelloWorldTimer(Timer):
    interval = 3   # Intervalo en segundos.
    
    # Función a ejecutar.
    def timer(self):
        print("¡Hola, mundo!")

Y para iniciarlo y detenerlo:

hello_world_timer = HelloWorldTimer()
hello_world_timer.start()   # Iniciar el timer.
time.sleep(10)
hello_world_timer.stop()    # Detenerlo.



1 comentario.

Deja una respuesta