Подтвердить что ты не робот

Прерывание потока в Python

Есть ли способ дождаться завершения потока, но все же перехватывать сигналы?

Рассмотрим следующую программу 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;
}

Он заканчивается через одну секунду и печатает:

Killing
Handling 10

Напротив, здесь моя попытка записать его в 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")

Он печатает:

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

Как заставить его вести себя как версия C?

4b9b3361

Ответ 1

Яррет Харди уже упомянул об этом: Согласно Guido van Rossum, там на данный момент лучше: Как указано в документации, join(None) блоки (а это означает отсутствие сигналов). Альтернатива - вызов с огромным таймаутом (join(2**31) или так) и проверка isAlive выглядит великолепно. Однако способ обработки таймеров Python является катастрофическим, как видно при запуске тестовой программы python с servth.join(100) вместо 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

I.e., Python просыпается каждые 50 мс, что приводит к тому, что одно приложение удерживает процессор от сна.

Ответ 2

Потоки в Python - несколько странные зверь с глобальным блокированием интерпретатора. Возможно, вы не сможете достичь того, чего хотите, не прибегая к тайм-ауту соединения и isAlive, как предлагает eliben.

В документах есть два места, которые дают причину этого (и, возможно, больше).

Первое:

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

Некоторая осторожность должна быть предпринята, если оба сигналы и потоки используются в той же программы. Фундаментальное значение для помните об использовании сигналов и потоков одновременно: всегда выполнять signal() в основном потоке исполнения. Любая нить может выполнять сигнал тревоги(), getsignal(), пауза(), setitimer() или getitimer(); только основной поток может установить новый сигнал обработчика, а основной поток будет только для приема сигналов (это обеспечивается сигналом Python модуль, даже если основной поток реализация поддерживает отправку сигналы к отдельным потокам). Эта означает, что сигналы не могут использоваться как средства межпоточной связи. Вместо этого используйте блокировки.

Второй, из http://docs.python.org/library/thread.html#module-thread:

Потоки взаимодействуют странно с прерываниями: исключение KeyboardInterrupt будет полученных произвольной нитью. (Когда доступен сигнальный модуль, прерывания всегда переходите к основной теме.)

РЕДАКТИРОВАТЬ: Было хорошее обсуждение механики этого на трекере python: http://bugs.python.org/issue1167930. Конечно, это заканчивается тем, что Гвидо говорит: "Это вряд ли уйдет, так что вам просто нужно будет жить с этим. Как вы обнаружили, определение тайм-аута решает проблему (вид). "YMMV: -)

Ответ 3

Опросите isAlive перед вызовом join. Конечно, этот опрос может быть прерван, и как только поток не isAlive, join будет немедленным.

Альтернативой может быть опрос на join с таймаутом, проверка с помощью isAlive, произошел ли тайм-аут. Это может потратить меньше процессора, чем предыдущий метод.

Ответ 5

Я знаю, что немного опаздываю на вечеринку, но я пришел к этому вопросу, надеясь получить лучший ответ, чем присоединиться к таймауту, который я уже делал. В конце концов я приготовил что-то, что может быть или не быть ужасным улавливанием сигналов, но оно включает в себя использование signal.pause() вместо Thread.join() и сигнализацию текущего процесса, когда поток достигает конца его выполнения:

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

Если вы хотите увидеть действия SIGTERM в действии, увеличьте продолжительность времени ожидания в thread_doer и выполните команду kill $pid с другого терминала, где $pid - идентификатор pid, напечатанный в начале.

Я размещаю это как можно больше, надеясь помочь другим, сказав, что это сумасшествие или ошибка. Я не уверен, что блокировка на threadcount все еще необходима - я положил ее на ранней стадии своих экспериментов и подумал, что я должен оставить ее там на всякий случай.