limitusus’s diary

主に技術のことを書きます

Python で signal と threading を両立してみた

経緯

最近書いているプログラムで無限ループするワーカースレッドを立てまくるものがあって, それを signal で安全に終了させる手段が知りたかった.

課題プログラム

以下のようなプログラムがあります.

import threading
import time
def loopfunc(event):
    print "Thread Started"
    while not event.isSet():
       time.sleep(1)
    print "Thread End"

def main() :
    threads = []
    e = threading.Event()
    for x in range(10):
        threads.append(threading.Thread(target=loopfunc, args=(e,)))
        threads[x].start()
    for th in threads:
        th.join()

main()

これを起動すると10個のスレッドが作られて, 無限ループします. これに対して SIGINT などを送っても何も起きません. これは Python の thread 全体に対していえることらしいです. たとえば明示的にシグナルハンドラを作って

import threading
import time
import signal
def loopfunc(event):
    print "Thread Started"
    while not event.isSet():
       time.sleep(1)
    print "Thread End"

def sighandler(event, signr, handler):
    event.set()

def main() :
    threads = []
    e = threading.Event()
    signal.signal(signal.SIGINT, (lambda a, b: sighandler(e, a, b)))
    for x in range(10):
        threads.append(threading.Thread(target=loopfunc, args=(e,)))
        threads[x].start()
    for th in threads:
        th.join()

main()

としても, やはり SIGINT は無視されます. これはなぜかというと, join() を呼んでいる最中は signal を受け付けないからです.

解決策

これは絶対に誰か出会った問題だろうと思って調べてみたら(下の参考ページ参照), 開始直後に os.fork() してシグナルハンドラを登録し, スレッドのマスターに SIGKILL を送信することによって解決するという方法が掲載されていました. しかしさすがにこれは乱暴すぎるような気がしたので, もう少し穏やかな終了方法を考えてみました. ふと見るとこれ, 先輩のページだ!

import threading
import time
import signal
def loopfunc(event):
    print "Thread Started"
    while not event.isSet():
       time.sleep(1)
    print "Thread End"

def sighandler(event, signr, handler):
    event.set()

def main() :
    threads = []
    e = threading.Event()
    signal.signal(signal.SIGINT, (lambda a, b: sighandler(e, a, b)))
    for x in range(10):
        threads.append(threading.Thread(target=loopfunc, args=(e,)))
        threads[x].start()
    for th in threads:
        while th.isAlive():
            time.sleep(0.5)
        th.join()

main()

このように 0.5 秒に1回, スレッドの生存を確認することによって join() を呼ばず, シグナルを受け付けられるようにしてみました. この他にも join([timeout]) に引数を渡して isAlive() で調べるループにするという方法もアリだと思います.

これがベストなのかどうかは分かりませんが, やりたいことは実現できたのでメモ.