2009-03-10 8 views
22

¿Hay alguna manera de esperar la terminación de un subproceso, pero aún así interceptar las señales?Unión de subprocesos interrumpibles en Python

Considere el siguiente programa C:

#include <signal.h> 
#include <stdio.h> 
#include <sys/types.h> 
#include <unistd.h> 
#include <pthread.h> 
#include <stdlib.h> 

void* server_thread(void* dummy) { 
    sleep(10); 
    printf("Served\n"); 
    return NULL; 
} 

void* kill_thread(void* dummy) { 
    sleep(1); // Let the main thread join 
    printf("Killing\n"); 
    kill(getpid(), SIGUSR1); 
    return NULL; 
} 

void handler(int signum) { 
    printf("Handling %d\n", signum); 
    exit(42); 
} 

int main() { 
    pthread_t servth; 
    pthread_t killth; 

    signal(SIGUSR1, handler); 

    pthread_create(&servth, NULL, server_thread, NULL); 
    pthread_create(&killth, NULL, kill_thread, NULL); 

    pthread_join(servth, NULL); 

    printf("Main thread finished\n"); 
    return 0; 
} 

Se termina después de un segundo y grabados:

Killing 
Handling 10 

Por el contrario, aquí está mi intento de escribir en Python:

#!/usr/bin/env python 
import signal, time, threading, os, sys 

def handler(signum, frame): 
    print("Handling " + str(signum) + ", frame:" + str(frame)) 
    exit(42) 
signal.signal(signal.SIGUSR1, handler) 

def server_thread(): 
    time.sleep(10) 
    print("Served") 
servth = threading.Thread(target=server_thread) 
servth.start() 

def kill_thread(): 
    time.sleep(1) # Let the main thread join 
    print("Killing") 
    os.kill(os.getpid(), signal.SIGUSR1) 
killth = threading.Thread(target=kill_thread) 
killth.start() 

servth.join() 

print("Main thread finished") 

Imprime:

Killing 
Served 
Handling 10, frame:<frame object at 0x12649c0> 

¿Cómo hacer que se comporte como la versión de C?

+0

'gcc -pthread thread.c' es la forma de compilar código fuente en C si alguien se enfrentó a errores como yo probando 'gcc thread.c' solo. – ViFI

Respuesta

5

Jarret Hardie ya mentioned it: De acuerdo con Guido van Rossum, no hay mejor manera a partir de ahora: Como se indica en las documentation, join(None) bloques (y eso significa que no hay señales). La alternativa: llamar con un gran tiempo de espera (join(2**31) más o menos) y marcar isAlive se ve genial. Sin embargo, la forma en Python maneja temporizadores es desastroso, como se ve cuando se ejecuta el programa de pruebas de pitón con servth.join(100) en lugar de servth.join():

select(0, NULL, NULL, NULL, {0, 1000}) = 0 (Timeout) 
select(0, NULL, NULL, NULL, {0, 2000}) = 0 (Timeout) 
select(0, NULL, NULL, NULL, {0, 4000}) = 0 (Timeout) 
select(0, NULL, NULL, NULL, {0, 8000}) = 0 (Timeout) 
select(0, NULL, NULL, NULL, {0, 16000}) = 0 (Timeout) 
select(0, NULL, NULL, NULL, {0, 32000}) = 0 (Timeout) 
select(0, NULL, NULL, NULL, {0, 50000}) = 0 (Timeout) 
select(0, NULL, NULL, NULL, {0, 50000}) = 0 (Timeout) 
select(0, NULL, NULL, NULL, {0, 50000}) = 0 (Timeout) 
--- Skipped 15 equal lines --- 
select(0, NULL, NULL, NULL, {0, 50000}Killing 

Es decir, Python se despierta cada 50 ms, lo que lleva a una única aplicación de mantenimiento de la CPU de dormir .

+0

En Python 3, 'servth.join()' bloqueado en ['lock.acquire()'] (https://docs.python.org/3/library/_thread.html#_thread.lock.acquire) puede ser interrumpido por una señal. – jfs

+0

estoy publicando para coincidir con @phihag (en python 2.6 centos-6): esperaba recibir señales cuando mi hilo principal hace un 'if thread.is_alive(): thread.join()' ... pero desafortunadamente no recibió la señal. Curiosamente cuando hago un 'while thread.is_alive(): thread.join (30.0)' recibo la señal como se esperaba. -> ** en resumen, encontré el mismo comportamiento que @phihag encontrado (es decir, tienes que usar 'thread.join' con un tiempo de espera ... de lo contrario no puedes recibir señales). ** –

+0

aquí está [otra respuesta ] (https://stackoverflow.com/a/29661280/52074) que acepta que debe usar una unión de subproceso con un tiempo de espera –

3

Encuesta en isAlive antes de llamar al join. Este sondeo se puede interrumpir, por supuesto, y una vez que el hilo no es isAlive, join es inmediato.

Una alternativa sería sondear en join con un tiempo de espera, verificando con isAlive si se ha agotado el tiempo de espera. Esto puede gastar menos CPU que el método anterior.

+1

Claro, el sondeo funciona, pero usa más recursos y evita el estado de suspensión de la CPU, que puede ser bastante costoso en los portátiles. Estoy buscando otra solución. – phihag

+0

sí, pero utilizando el segundo método no desperdicia CPU, porque se une con bloques de tiempo de espera y lo libera. así que incluso un tiempo de espera relativamente pequeño de algunas docenas de milisegundos te dejará el 99.9% de la CPU libre –

+0

eliben: 99.9% de CPU libre no es lo menos deseable si el trabajo se distribuye uniformemente, preferiría mucho 80% libre de CPU en una sola ráfaga para una aplicación de escritorio. Ver http://www.lesswatts.org/projects/applications-power-management/avoid-pulling.php para más detalles. – phihag

14

Los hilos en Python son bestias algo extrañas dado el bloqueo de intérprete global. Es posible que no pueda lograr lo que quiere sin recurrir a un tiempo de espera de unirse y esAlive como lo sugiere eliben.

Hay dos puntos en los documentos que dan la razón de esto (y posiblemente más).

La primera:

De http://docs.python.org/library/signal.html#module-signal:

hay que tener cierto cuidado si se utilizan ambos señales y los hilos en el mismo programa . Lo fundamental para recordar al usar señales e hilos simultáneamente es: realizar siempre operaciones de señal () en el hilo principal de ejecución. Cualquier hilo puede realizar una alarma(), getsignal(), pausa(), setitimer() o getitimer(); sólo el hilo principal se estableció un nuevo manejador de señales , y el hilo principal será el único en recibir señales (esto es impuesta por el módulo de Python señal , incluso si el subyacente implementación de hilos admite el envío señales a hilos individuales). Esto significa que las señales no se pueden usar como un medio de comunicación entre hilos. Use bloqueos en su lugar.

El segundo, desde http://docs.python.org/library/thread.html#module-thread:

Hilos interactúan extrañamente con interrupciones: la excepción KeyboardInterrupt será recibido por un hilo arbitrario. (Cuando el módulo de señal está disponible, interrumpe siempre van al hilo principal.)

EDIT: Hubo una discusión decente de la mecánica de este en el seguimiento de errores pitón aquí: http://bugs.python.org/issue1167930. Por supuesto, termina con Guido diciendo: "Es poco probable que desaparezca, por lo que tendrás que vivir con esto. Como has descubierto, especificar un tiempo de espera resuelve el problema (más o menos)". YMMV :-)

+0

Bueno, I * estoy * llamando signal.signal en el hilo principal (1), y el módulo de señal está disponible (2). – phihag

+0

Derecha, pero la señal solo irá al hilo principal, por lo que tendrá que esperar que se una el servth antes de que la señal vaya al manejador de señal (a través del hilo principal). Confuso, ¿no? –

0

Sé que llego un poco tarde a la fiesta, pero llegué a esta pregunta con la esperanza de obtener una mejor respuesta que unirme al tiempo de espera, que ya estaba haciendo. En el extremo Cociné algo que pueden o no puede ser un bastardisation terrible de señales, pero implica el uso de signal.pause() en lugar de Thread.join() y la señalización del proceso actual cuando el hilo alcanza el final de su ejecución:

import signal, os, time, sys, threading, random 

threadcount = 200 

threadlock = threading.Lock() 
pid = os.getpid() 
sigchld_count = 0 

def handle_sigterm(signalnum, frame): 
    print "SIGTERM" 

def handle_sigchld(signalnum, frame): 
    global sigchld_count 
    sigchld_count += 1 

def faux_join(): 
    global threadcount, threadlock 
    threadlock.acquire() 
    threadcount -= 1 
    threadlock.release() 
    os.kill(pid, signal.SIGCHLD) 

def thread_doer(): 
    time.sleep(2+(2*random.random())) 
    faux_join() 

if __name__ == '__main__': 
    signal.signal(signal.SIGCHLD, handle_sigchld) 
    signal.signal(signal.SIGTERM, handle_sigterm) 

    print pid 
    for i in xrange(0, threadcount): 
     t = threading.Thread(target=thread_doer) 
     t.start() 

    while 1: 
     if threadcount == 0: break 
     signal.pause() 
     print "Signal unpaused, thread count %s" % threadcount 

    print "All threads finished" 
    print "SIGCHLD handler called %s times" % sigchld_count 

Si desea ver los SIGTERM en acción, ampliar la duración del tiempo de inactividad en thread_doer y emitir un comando kill $pid desde otro terminal, donde $pid es el ID de pid impreso al inicio.

Publico esto con la esperanza de ayudar a otros a decirles que esto es una locura o que tiene un error. No estoy seguro de si el bloqueo en la cuenta de hilos todavía es necesario. Lo puse allí temprano en mi experimentación y pensé que debería dejarlo allí por si acaso.

1

Por lo que yo entiendo, una pregunta similar se resuelve en The Little Book of Semaphores (descarga gratuita), apéndice A parte 3 ...

+0

. Sin embargo, eso es bastante complicado, básicamente reemplazando la comunicación entre procesos con – phihag

Cuestiones relacionadas