web-dev-qa-db-ja.com

マルチスレッドでのシグナル処理Python

これは非常に簡単なはずで、stackoverflowで既に答えられたこの質問を見つけることができなかったことに非常に驚いています。

Upstartでうまく動作するために、SIGTERMおよびSIGINTシグナルに応答する必要があるプログラムのようなデーモンがあります。これを行う最善の方法は、メインスレッドとは別のスレッドでプログラムのメインループを実行し、メインスレッドに信号を処理させることだと読みました。次に、シグナルが受信されると、シグナルハンドラはメインループで定期的にチェックされているセンチネルフラグを設定して、メインループに終了するように指示する必要があります。

私はこれをやってみましたが、期待どおりに機能していません。以下のコードを参照してください。

from threading import Thread
import signal
import time
import sys

stop_requested = False    

def sig_handler(signum, frame):
    sys.stdout.write("handling signal: %s\n" % signum)
    sys.stdout.flush()

    global stop_requested
    stop_requested = True    

def run():
    sys.stdout.write("run started\n")
    sys.stdout.flush()
    while not stop_requested:
        time.sleep(2)

    sys.stdout.write("run exited\n")
    sys.stdout.flush()

signal.signal(signal.SIGTERM, sig_handler)
signal.signal(signal.SIGINT, sig_handler)

t = Thread(target=run)
t.start()
t.join()
sys.stdout.write("join completed\n")
sys.stdout.flush()

これを次の2つの方法でテストしました。

1)

$ python main.py > output.txt&
[2] 3204
$ kill -15 3204

2)

$ python main.py
ctrl+c

どちらの場合も、これが出力に書き込まれることを期待しています。

run started
handling signal: 15
run exited
join completed

最初の場合、プログラムは終了しますが、表示されるのは次のとおりです。

run started

2番目のケースでは、ctrl + cが押されてプログラムが終了しない場合、SIGTERMシグナルは無視されるようです。

ここに何が欠けていますか?

24
stuckintheshuck

問題は、 Pythonシグナルハンドラーの実行 で説明されているとおりです。

Pythonシグナルハンドラーは、低レベル(C)シグナルハンドラー内では実行されません。代わりに、低レベルのシグナルハンドラーは、対応するPythonシグナルハンドラーを後のポイント(たとえば、次のバイトコード命令)で実行するように仮想マシンに指示するフラグを設定します

純粋にCで実装された長時間実行の計算(大量のテキストに対する正規表現マッチングなど)は、受信した信号に関係なく、任意の時間中断なしで実行される場合があります。 Pythonシグナルハンドラーは、計算が終了すると呼び出されます。

メインスレッドはthreading.Thread.joinでブロックされます。つまり、最終的にはpthread_join呼び出しでCでブロックされます。もちろん、それは「長時間実行される計算」ではなく、システムコールのブロックです…しかし、それでも、その呼び出しが完了するまで、シグナルハンドラは実行できません。

また、一部のプラットフォームではpthread_joinが信号でEINTRで失敗しますが、他のプラットフォームでは失敗しません。 Linuxでは、BSDスタイルを選択するかデフォルトのsiginterrupt動作を選択するかによって異なりますが、デフォルトはnoです。


だから、あなたはそれについて何ができますか?

Python 3.3でのシグナル処理の変更 は、Linuxのデフォルトの動作を実際に変更したので、アップグレードしても何もする必要はありません。 3.3以降で実行するだけで、コードは期待どおりに機能します。少なくとも、OS XではCPython 3.4、Linuxでは3.3を使用します。 (これについて間違っている場合、それがCPythonのバグであるかどうかわからないので、問題を開くのではなくpython-listで上げることをお勧めします…)

一方、3.3より前のsignalモジュールは、この問題を自分で解決するために必要なツールを公開していません。したがって、3.3にアップグレードできない場合の解決策は、ConditionEventなどの割り込み可能なものを待つことです。子スレッドは終了する直前にイベントを通知し、メインスレッドは子スレッドに参加する前にイベントを待機します。これは間違いなくハッキーです。そして、私はそれが違いを生むことを保証するものを見つけることができません。 OS XではCPython 2.7および3.2、Linuxでは2.6および2.7のさまざまなビルドで動作するようになりました…

30
abarnert

abarnertの答えはスポットオンでした。私はまだPython 2.7を使用しています。この問題を自分で解決するために、InterruptableThreadクラスを作成しました。

現時点では、スレッドターゲットに追加の引数を渡すことはできません。結合もタイムアウトパラメータを受け入れません。これは、私がそれをする必要がないからです。必要に応じて追加できます。これを自分で使用する場合は、おそらく出力ステートメントを削除する必要があります。コメントとテストの方法として、そこにあります。

import threading
import signal
import sys

class InvalidOperationException(Exception):
    pass    

# noinspection PyClassHasNoInit
class GlobalInterruptableThreadHandler:
    threads = []
    initialized = False

    @staticmethod
    def initialize():
        signal.signal(signal.SIGTERM, GlobalInterruptableThreadHandler.sig_handler)
        signal.signal(signal.SIGINT, GlobalInterruptableThreadHandler.sig_handler)
        GlobalInterruptableThreadHandler.initialized = True

    @staticmethod
    def add_thread(thread):
        if threading.current_thread().name != 'MainThread':
            raise InvalidOperationException("InterruptableThread objects may only be started from the Main thread.")

        if not GlobalInterruptableThreadHandler.initialized:
            GlobalInterruptableThreadHandler.initialize()

        GlobalInterruptableThreadHandler.threads.append(thread)

    @staticmethod
    def sig_handler(signum, frame):
        sys.stdout.write("handling signal: %s\n" % signum)
        sys.stdout.flush()

        for thread in GlobalInterruptableThreadHandler.threads:
            thread.stop()

        GlobalInterruptableThreadHandler.threads = []    

class InterruptableThread:
    def __init__(self, target=None):
        self.stop_requested = threading.Event()
        self.t = threading.Thread(target=target, args=[self]) if target else threading.Thread(target=self.run)

    def run(self):
        pass

    def start(self):
        GlobalInterruptableThreadHandler.add_thread(self)
        self.t.start()

    def stop(self):
        self.stop_requested.set()

    def is_stop_requested(self):
        return self.stop_requested.is_set()

    def join(self):
        try:
            while self.t.is_alive():
                self.t.join(timeout=1)
        except (KeyboardInterrupt, SystemExit):
            self.stop_requested.set()
            self.t.join()

        sys.stdout.write("join completed\n")
        sys.stdout.flush()

このクラスは2つの異なる方法で使用できます。 InterruptableThreadをサブクラス化できます:

import time
import sys
from interruptable_thread import InterruptableThread

class Foo(InterruptableThread):
    def __init__(self):
        InterruptableThread.__init__(self)

    def run(self):
        sys.stdout.write("run started\n")
        sys.stdout.flush()
        while not self.is_stop_requested():
            time.sleep(2)

        sys.stdout.write("run exited\n")
        sys.stdout.flush()

sys.stdout.write("all exited\n")
sys.stdout.flush()

foo = Foo()
foo2 = Foo()
foo.start()
foo2.start()
foo.join()
foo2.join()

または、threading.threadの動作のように使用できます。ただし、runメソッドは、InterruptableThreadオブジェクトをパラメーターとして取得する必要があります。

import time
import sys
from interruptable_thread import InterruptableThread

def run(t):
    sys.stdout.write("run started\n")
    sys.stdout.flush()
    while not t.is_stop_requested():
        time.sleep(2)

    sys.stdout.write("run exited\n")
    sys.stdout.flush()

t1 = InterruptableThread(run)
t2 = InterruptableThread(run)
t1.start()
t2.start()
t1.join()
t2.join()

sys.stdout.write("all exited\n")
sys.stdout.flush()

あなたがすることでそれでやりなさい。

12
stuckintheshuck

私はここで同じ問題に直面しました 複数のスレッドが参加するときに信号が処理されませんabarnert の答えを読んだ後、Python 3に変更して問題を解決しました。しかし、すべてのプログラムをpython 3.そこで、シグナルを送信する前にスレッドjoin()を呼び出さないようにして、プログラムを解決しました。

それはあまり良くありませんが、python 2.7で私のプログラムを解決しました。私の質問は重複しているとマークされたので、ここに私のソリューションを置きます。

import threading, signal, time, os


RUNNING = True
threads = []

def monitoring(tid, itemId=None, threshold=None):
    global RUNNING
    while(RUNNING):
        print "PID=", os.getpid(), ";id=", tid
        time.sleep(2)
    print "Thread stopped:", tid


def handler(signum, frame):
    print "Signal is received:" + str(signum)
    global RUNNING
    RUNNING=False
    #global threads

if __name__ == '__main__':
    signal.signal(signal.SIGUSR1, handler)
    signal.signal(signal.SIGUSR2, handler)
    signal.signal(signal.SIGALRM, handler)
    signal.signal(signal.SIGINT, handler)
    signal.signal(signal.SIGQUIT, handler)

    print "Starting all threads..."
    thread1 = threading.Thread(target=monitoring, args=(1,), kwargs={'itemId':'1', 'threshold':60})
    thread1.start()
    threads.append(thread1)
    thread2 = threading.Thread(target=monitoring, args=(2,), kwargs={'itemId':'2', 'threshold':60})
    thread2.start()
    threads.append(thread2)
    while(RUNNING):
        print "Main program is sleeping."
        time.sleep(30)
    for thread in threads:
        thread.join()

    print "All threads stopped."
2
BAE