Esto debería ser muy simple y estoy muy sorprendido de que no haya podido encontrar estas preguntas ya respondidas en stackoverflow.

Tengo un programa similar a un demonio que necesita responder a las señales SIGTERM y SIGINT para funcionar bien con el arranque. Leí que la mejor manera de hacer esto es ejecutar el bucle principal del programa en un hilo separado del hilo principal y dejar que el hilo principal maneje las señales. Luego, cuando se recibe una señal, el manejador de señales debe indicarle al bucle principal que salga configurando una bandera centinela que se verifica rutinariamente en el bucle principal.

He intentado hacer esto, pero no funciona como esperaba. Vea el código a continuación:

from threading import Thread
import signal
import time
import sys

stop_requested = False    

def sig_handler(signum, frame):
    sys.stdout.write("handling signal: %s\n" % signum)
    sys.stdout.flush()

    global stop_requested
    stop_requested = True    

def run():
    sys.stdout.write("run started\n")
    sys.stdout.flush()
    while not stop_requested:
        time.sleep(2)

    sys.stdout.write("run exited\n")
    sys.stdout.flush()

signal.signal(signal.SIGTERM, sig_handler)
signal.signal(signal.SIGINT, sig_handler)

t = Thread(target=run)
t.start()
t.join()
sys.stdout.write("join completed\n")
sys.stdout.flush()

Probé esto de las siguientes dos maneras:

1)

$ python main.py > output.txt&
[2] 3204
$ kill -15 3204

2)

$ python main.py
ctrl+c

En ambos casos, espero que esto esté escrito en la salida:

run started
handling signal: 15
run exited
join completed

En el primer caso, el programa sale pero todo lo que veo es:

run started

En el segundo caso, la señal SIGTERM aparentemente se ignora cuando se presiona ctrl + c y el programa no sale.

¿Qué me estoy perdiendo aquí?

25
stuckintheshuck 5 sep. 2014 a las 04:12

3 respuestas

La mejor respuesta

El problema es que, como se explica en Ejecución de manejadores de señales Python :

Un controlador de señal de Python no se ejecuta dentro del controlador de señal de bajo nivel (C). En cambio, el controlador de señal de bajo nivel establece un indicador que le dice a la máquina virtual que ejecute el controlador de señal Python correspondiente en un punto posterior (por ejemplo, en la siguiente instrucción de código de bytes)

...

Un cálculo de larga duración implementado exclusivamente en C (como la coincidencia de expresiones regulares en un gran cuerpo de texto) puede ejecutarse sin interrupciones durante un período de tiempo arbitrario, independientemente de las señales recibidas. Se llamará a los manejadores de señales de Python cuando finalice el cálculo.

Su hilo principal está bloqueado en threading.Thread.join, lo que en última instancia significa que está bloqueado en C en una llamada pthread_join. Por supuesto, eso no es un "cálculo a largo plazo", es un bloque en una llamada al sistema ... pero, sin embargo, hasta que finalice la llamada, su controlador de señal no puede ejecutarse.

Y, mientras que en algunas plataformas pthread_join fallará con EINTR en una señal, en otras no lo hará. En Linux, creo que depende de si seleccionas el estilo BSD o el comportamiento predeterminado siginterrupt, pero el valor predeterminado es no.


¿Entonces, qué puede hacer usted al respecto?

Bueno, estoy bastante seguro de que los cambios en el manejo de la señal en Python 3.3 realmente cambiaron el comportamiento predeterminado en Linux, por lo que no tendrá que hacer nada si actualiza; simplemente ejecute por debajo de 3.3+ y su código funcionará como espera. Al menos lo hace para mí con CPython 3.4 en OS X y 3.3 en Linux. (Si me equivoco al respecto, no estoy seguro de si es un error en CPython o no, por lo que es posible que desee plantearlo en la lista de Python en lugar de abrir un problema ...)

Por otro lado, antes de 3.3, el módulo signal definitivamente no expone las herramientas que necesitaría para solucionar este problema usted mismo. Entonces, si no puede actualizar a 3.3, la solución es esperar algo interrumpible, como Condition o Event. El subproceso secundario notifica el evento justo antes de que se cierre, y el subproceso principal espera el evento antes de que se una al subproceso secundario. Esto es definitivamente hacky. Y no puedo encontrar nada que garantice que haga la diferencia; Simplemente funciona para mí en varias versiones de CPython 2.7 y 3.2 en OS X y 2.6 y 2.7 en Linux ...

31
abarnert 5 sep. 2014 a las 00:41

Enfrenté el mismo problema aquí la señal no se maneja cuando se unen varios hilos. Después de leer la respuesta de abarnert, cambié a Python 3 y resolví el problema. Pero me gusta cambiar todo mi programa a Python 3. Entonces, resolví mi programa evitando llamar a thread join () antes de enviar la señal. Abajo está mi código.

No es muy bueno, pero resolvió mi programa en Python 2.7. Mi pregunta se marcó como duplicada, así que puse mi solución aquí.

import threading, signal, time, os


RUNNING = True
threads = []

def monitoring(tid, itemId=None, threshold=None):
    global RUNNING
    while(RUNNING):
        print "PID=", os.getpid(), ";id=", tid
        time.sleep(2)
    print "Thread stopped:", tid


def handler(signum, frame):
    print "Signal is received:" + str(signum)
    global RUNNING
    RUNNING=False
    #global threads

if __name__ == '__main__':
    signal.signal(signal.SIGUSR1, handler)
    signal.signal(signal.SIGUSR2, handler)
    signal.signal(signal.SIGALRM, handler)
    signal.signal(signal.SIGINT, handler)
    signal.signal(signal.SIGQUIT, handler)

    print "Starting all threads..."
    thread1 = threading.Thread(target=monitoring, args=(1,), kwargs={'itemId':'1', 'threshold':60})
    thread1.start()
    threads.append(thread1)
    thread2 = threading.Thread(target=monitoring, args=(2,), kwargs={'itemId':'2', 'threshold':60})
    thread2.start()
    threads.append(thread2)
    while(RUNNING):
        print "Main program is sleeping."
        time.sleep(30)
    for thread in threads:
        thread.join()

    print "All threads stopped."
2
Community 23 may. 2017 a las 11:55

La respuesta de abarnert fue acertada. Sin embargo, todavía estoy usando Python 2.7. Para resolver este problema por mí mismo, escribí una clase InterruptableThread.

En este momento no permite pasar argumentos adicionales al objetivo del hilo. Join tampoco acepta un parámetro de tiempo de espera. Esto es solo porque no necesito hacer eso. Puedes agregarlo si quieres. Probablemente desee eliminar las declaraciones de salida si lo usa usted mismo. Simplemente están ahí como una forma de comentar y probar.

import threading
import signal
import sys

class InvalidOperationException(Exception):
    pass    

# noinspection PyClassHasNoInit
class GlobalInterruptableThreadHandler:
    threads = []
    initialized = False

    @staticmethod
    def initialize():
        signal.signal(signal.SIGTERM, GlobalInterruptableThreadHandler.sig_handler)
        signal.signal(signal.SIGINT, GlobalInterruptableThreadHandler.sig_handler)
        GlobalInterruptableThreadHandler.initialized = True

    @staticmethod
    def add_thread(thread):
        if threading.current_thread().name != 'MainThread':
            raise InvalidOperationException("InterruptableThread objects may only be started from the Main thread.")

        if not GlobalInterruptableThreadHandler.initialized:
            GlobalInterruptableThreadHandler.initialize()

        GlobalInterruptableThreadHandler.threads.append(thread)

    @staticmethod
    def sig_handler(signum, frame):
        sys.stdout.write("handling signal: %s\n" % signum)
        sys.stdout.flush()

        for thread in GlobalInterruptableThreadHandler.threads:
            thread.stop()

        GlobalInterruptableThreadHandler.threads = []    

class InterruptableThread:
    def __init__(self, target=None):
        self.stop_requested = threading.Event()
        self.t = threading.Thread(target=target, args=[self]) if target else threading.Thread(target=self.run)

    def run(self):
        pass

    def start(self):
        GlobalInterruptableThreadHandler.add_thread(self)
        self.t.start()

    def stop(self):
        self.stop_requested.set()

    def is_stop_requested(self):
        return self.stop_requested.is_set()

    def join(self):
        try:
            while self.t.is_alive():
                self.t.join(timeout=1)
        except (KeyboardInterrupt, SystemExit):
            self.stop_requested.set()
            self.t.join()

        sys.stdout.write("join completed\n")
        sys.stdout.flush()

La clase se puede usar de dos maneras diferentes. Puede subclase InterruptableThread:

import time
import sys
from interruptable_thread import InterruptableThread

class Foo(InterruptableThread):
    def __init__(self):
        InterruptableThread.__init__(self)

    def run(self):
        sys.stdout.write("run started\n")
        sys.stdout.flush()
        while not self.is_stop_requested():
            time.sleep(2)

        sys.stdout.write("run exited\n")
        sys.stdout.flush()

sys.stdout.write("all exited\n")
sys.stdout.flush()

foo = Foo()
foo2 = Foo()
foo.start()
foo2.start()
foo.join()
foo2.join()

O puede usarlo más como funciona threading.thread. Sin embargo, el método de ejecución debe tomar el objeto InterruptableThread como parámetro.

import time
import sys
from interruptable_thread import InterruptableThread

def run(t):
    sys.stdout.write("run started\n")
    sys.stdout.flush()
    while not t.is_stop_requested():
        time.sleep(2)

    sys.stdout.write("run exited\n")
    sys.stdout.flush()

t1 = InterruptableThread(run)
t2 = InterruptableThread(run)
t1.start()
t2.start()
t1.join()
t2.join()

sys.stdout.write("all exited\n")
sys.stdout.flush()

Haz con eso lo que quieras.

12
stuckintheshuck 5 sep. 2014 a las 18:05