Прерывание потока в 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?
Ответы
Ответ 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
, произошел ли тайм-аут. Это может потратить меньше процессора, чем предыдущий метод.
Ответ 4
Насколько я понимаю, аналогичный вопрос решается в "Маленькая книга семафоров" (скачать бесплатно), добавление A часть 3...
Ответ 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 все еще необходима - я положил ее на ранней стадии своих экспериментов и подумал, что я должен оставить ее там на всякий случай.