Catch KeyboardInterrupt or handle signal in thread

你离开我真会死。 提交于 2019-12-23 03:12:26

问题


I have some threads running, and one of those threads contains an object that will be spawning subprocesses. I want one such subprocess to be able to kill the entire application. The aforementioned object will need to save some state when it receives this signal. Unfortunately I can't get the signal to be handled in the thread that causes the kill.

Here is some example code that attempts to replicate the situation.

parent.py: starts a thread. that thread runs some subprocesses, one of which will try to kill the parent process.

#!/usr/local/bin/python3
import subprocess, time, threading, random

def killer_func():
    possible_cmds = [['echo', 'hello'],
                     ['echo', 'world'],
                     ['/work/turbulencetoo/tmp/killer.py']
                     ]
    random.shuffle(possible_cmds)
    for cmd in possible_cmds:
        try:
            time.sleep(2)
            subprocess.check_call(cmd)
            time.sleep(2)
        except KeyboardInterrupt:
            print("Kill -2 caught properly!!")
            print("Here I could properly save my state")
            break
        except Exception as e:
            print("Unhandled Exception: {}".format(e))
        else:
            print("No Exception")

killer_thread = threading.Thread(target=killer_func)
killer_thread.start()
try:
    while True:
        killer_thread.join(4)
        if not killer_thread.is_alive():
            print("The killer thread has died")
            break
        else:
            print("Killer thread still alive, try to join again.")
except KeyboardInterrupt:
    print("Caught the kill -2 in the main thread :(")

print("Main program shutting down")

killer.py, a simple program that tries to kill its parent process with SIGINT:

#!/usr/local/bin/python3
import time, os, subprocess, sys

ppid = os.getppid()

# -2 specifies SIGINT, python handles this as a KeyboardInterrupt exception
cmd = ["kill", "-2", "{}".format(ppid)]

subprocess.check_call(cmd)
time.sleep(3)

sys.exit(0)

Here is some sample output from running the parent program:

$ ./parent.py
hello
Killer thread still alive, try to join again.
No Exception
Killer thread still alive, try to join again.
Caught the kill -2 in the main thread :(
Main program shutting down
No Exception
world
No Exception

I've tried using signal.signal() inside killer_func, but it doesn't work in a sub thread.

Is there a way to force the signal or exception to be handled by the function without the main thread being aware?


回答1:


The main thread of your program will always be the one that receives the signal. The signal module documentation states this:

Some care must be taken if both signals and threads are used in the same program. The fundamental thing to remember in using signals and threads simultaneously is: always perform signal() operations in the main thread of execution. Any thread can perform an alarm(), getsignal(), pause(), setitimer() or getitimer(); only the main thread can set a new signal handler, and the main thread will be the only one to receive signals (this is enforced by the Python signal module, even if the underlying thread implementation supports sending signals to individual threads). This means that signals can’t be used as a means of inter-thread communication. Use locks instead.

You'll need to refactor your program such that the main thread receiving the signal doesn't prevent you from saving state. The easiest way is use something like threading.Event() to tell the background thread that the program has been aborted, and let it clean up when it sees the event has been set:

import subprocess
import threading
import random

def killer_func(event):
    possible_cmds = [['echo', 'hello'],
                     ['echo', 'world'],
                     ['/home/cycdev/killer.py']
                     ]
    random.shuffle(possible_cmds)
    for cmd in possible_cmds:
        subprocess.check_call(cmd)
        event.wait(4)
        if event.is_set():
            print("Main thread got a signal. Time to clean up")
            # save state here.
            return

event = threading.Event()
killer_thread = threading.Thread(target=killer_func, args=(event,))
killer_thread.start()
try:
    killer_thread.join()
except KeyboardInterrupt:
    print("Caught the kill -2 in the main thread :)")
    event.set()
    killer_thread.join()

print("Main program shutting down")



回答2:


Signals are always handled in the main thread. When you receive a signal, you don't know where it comes from. You can't say "handle it in the thread that spawned the signal-sending-process" because you don't know what signal-sending-process is.

The way to solve this is to use Condition Variables to notify all threads that a signal was received and that they have to shut down.

import threading

got_interrupt = False   # global variable

def killer_func(cv):
    ...
    with cv:
        cv.wait(2)
        interupted = got_interrupt  # Read got_interrupt while holding the lock
    if interrupted:
        cleanup()
    ...


lock = threading.Lock()
notifier_cv = threading.Condition(lock)
killer_thread = threading.Thread(target=killer_func, args=(notifier_cv,))
killer_thread.start()
try:
    ...
except KeyboardInterrupt:
    with cv:
        got_interrupt = True
        cv.notify_all()


来源:https://stackoverflow.com/questions/30267647/catch-keyboardinterrupt-or-handle-signal-in-thread

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!