Прерывистый поток присоединиться в Python - PullRequest
23 голосов
/ 10 марта 2009

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

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

Ответы [ 5 ]

14 голосов
/ 10 марта 2009

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

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

Первый:

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

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

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

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

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

6 голосов
/ 11 марта 2009

Джаррет Харди уже упомянул это : Согласно Гвидо ван Россум , лучшего способа на данный момент нет: как указано в документации , 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

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

3 голосов
/ 10 марта 2009

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

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

1 голос
/ 21 февраля 2011

Насколько я понимаю, похожий вопрос решен в Маленькая книга семафоров (скачать бесплатно), приложение Часть 3…

0 голосов
/ 21 февраля 2011

Я знаю, что немного опоздал на вечеринку, но я пришел к этому вопросу, надеясь получить лучший ответ, чем присоединиться к тайм-ауту, который я уже делал. В конце концов, я приготовил что-то, что может или не может быть ужасной бастардизацией сигналов, но это включает использование 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, напечатанный в начале.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...