2010-09-14 10 views
5

He visto un montón de preguntas relacionadas con esto ... pero mi código funciona en python 2.6.2 y falla para trabajar en Python 2.6.5. ¿Me equivoco al pensar que todas las funciones de atexit registradas a través de este módulo no se invocan cuando el programa se apaga por una señal? ¿Algo que no debería contar aquí porque capto la señal y luego salgo limpiamente? ¿Que está pasando aqui? ¿Cuál es la forma correcta de hacer esto?python 2.6.x theading/signals/atexit ¿falla en algunas versiones?

import atexit, sys, signal, time, threading 

terminate = False 
threads = [] 

def test_loop(): 
    while True: 
     if terminate: 
      print('stopping thread') 
      break 
     else: 
      print('looping') 
      time.sleep(1) 

@atexit.register 
def shutdown(): 
    global terminate 
    print('shutdown detected') 
    terminate = True 
    for thread in threads: 
     thread.join() 

def close_handler(signum, frame): 
    print('caught signal') 
    sys.exit(0) 

def run(): 
    global threads 
    thread = threading.Thread(target=test_loop) 
    thread.start() 
    threads.append(thread) 

    while True: 
     time.sleep(2) 
     print('main') 

signal.signal(signal.SIGINT, close_handler) 

if __name__ == "__main__": 
    run() 

Python 2.6.2:

$ python halp.py 
looping 
looping 
looping 
main 
looping 
main 
looping 
looping 
looping 
main 
looping 
^Ccaught signal 
shutdown detected 
stopping thread 

Python 2.6.5:

$ python halp.py 
looping 
looping 
looping 
main 
looping 
looping 
main 
looping 
looping 
main 
^Ccaught signal 
looping 
looping 
looping 
looping 
... 
looping 
looping 
Killed <- kill -9 process at this point 

El hilo principal en 2.6.5 parece que nunca se ejecutará las funciones atexit.

+0

He probado el código en Python 2.6.5 y 2.6.1 de Python en OS X 10.6, y se comportan del mismo modo descrito en la pregunta (2.6.5 no ejecutar la atexit mientras hace 2.6.1). Espero que las personas más versadas en el código fuente de Python aconsejen sobre lo que ha cambiado. –

+0

puede haber perdido interés en esto o encontrado una solución, pero todavía estoy interesado en qué cambió entre las dos versiones de Python para desencadenar esto. En lugar de hacer la misma pregunta nuevamente, voy a comenzar una recompensa por esto. Espero que no le importe. –

Respuesta

7

La diferencia de raíz aquí en realidad no está relacionada con ambas señales y atexit, sino más bien con un cambio en el comportamiento de sys.exit.

Antes de aproximadamente 2.6.5, sys.exit (más exactamente, SystemExit siendo capturado en el nivel superior) haría que el intérprete salga; si los hilos seguían ejecutándose, se terminarían, al igual que con los hilos POSIX.

Alrededor de 2.6.5, el comportamiento cambió: el efecto de sys.exit ahora es esencialmente el mismo que el de regresar de la función principal del programa. Cuando haga que - en ambas versiones - el intérprete espera a que todos los hilos se unan antes de salir.

El cambio relevante es que Py_Finalize ahora llama a wait_for_thread_shutdown() cerca de la parte superior, donde no estaba antes.

Este cambio de comportamiento no parece correcta, sobre todo porque ya no funciona como se documenta, que es simplemente: "Salir desde Python." El efecto práctico ya no es salir de Python, sino simplemente salir del hilo. (Como nota al margen, sys.exit nunca salió de Python cuando se lo llamó desde otro subproceso, pero esa oscura divergencia del comportamiento documentado no justifica una mucho más grande).

Veo el atractivo del nuevo comportamiento: en lugar de dos formas de salir del hilo principal ("salir y esperar por los hilos" y "salir inmediatamente"), solo hay uno, ya que sys.exit es esencialmente idéntico a simplemente regresar de la función superior. Sin embargo, es un cambio radical y difiere del comportamiento documentado, que supera con creces.

Debido a este cambio, después de sys.exit desde el controlador de señal anterior, el intérprete se sienta esperando que salgan los hilos y luego ejecuta los controladores atexit después de que lo hagan. Dado que es el manejador el que le dice a los hilos que salgan, el resultado es un punto muerto.

+1

Muchas gracias, Glenn. Ahora que sé lo que debo buscar, encuentro el informe relevante de Python [aquí] (http://bugs.python.org/issue1722344). Estoy de acuerdo en que es un gran cambio que debería haberse realizado en más de un lanzamiento de punto menor. –

0

No No estoy seguro si esto fue cambiado por completo, pero esto es como yo he hecho mi atexit en 2.6.5


atexit.register(goodbye) 

def goodbye(): 
    print "\nStopping..." 
+1

desde 2.6 atexit.register se puede usar como decorador. – lostincode

+0

Hmm, bueno eso es extraño. ¿Estás seguro de que estás ejecutando el mismo código y no está en la memoria caché en otro lugar o algo extraño como ese? – Falmarri

3

salir debido a una señal es la misma que sale de dentro de un manejador de señal. Capturar una señal y salir con sys.exit es una salida limpia, no una salida debido a un manejador de señal. Entonces, sí, estoy de acuerdo en que debería ejecutar controladores de atexit aquí, al menos en principio.

Sin embargo, hay algo complicado sobre los manejadores de señal: son completamente asincrónicos. Pueden interrumpir el flujo del programa en cualquier momento, entre cualquier código de operación de VM. Toma este código, por ejemplo. (Esto se debe manejar como la misma forma que el código anterior; he omitido código por razones de brevedad.)

import threading 
lock = threading.Lock() 
def test_loop(): 
    while not terminate: 
     print('looping') 
     with lock: 
      print "Executing synchronized operation" 
     time.sleep(1) 
    print('stopping thread') 

def run(): 
    while True: 
     time.sleep(2) 
     with lock: 
      print "Executing another synchronized operation" 
     print('main') 

Hay un problema serio aquí: una señal puede ser recibida mientras run() es (por ejemplo,^C). sosteniendo lock. Si eso sucede, su manejador de señal se ejecutará con el bloqueo aún retenido. Luego esperará a que test_loop salga, y si ese hilo está esperando el bloqueo, tendrá un punto muerto.

Esta es toda una categoría de problemas, y es por eso que un lote de API dice no llamarlos desde dentro de controladores de señal. En su lugar, debe establecer un indicador para indicarle al hilo principal que se apague en el momento apropiado.

do_shutdown = False 
def close_handler(signum, frame): 
    global do_shutdown 
    do_shutdown = True 
    print('caught signal') 

def run(): 
    while not do_shutdown: 
     ... 

Mi preferencia es evitar salir del programa con sys.exit por completo y hacer explícita la limpieza en el punto de salida principal (por ejemplo. El final de carrera()), pero se puede usar atexit aquí si quieres .