PythonでThread終了後に後処理をしたい
簡単な処理を書いてる分にはあまり気にしないのだけれども、ちょっとしたアプリを作成する時にハマりがちなメモ
ちょっとしたアプリを作成する時にはシグナルを受信後に後処理をしてから終了、とかやるのだがPython2系は特殊だから気をつけないといけない。
環境
OS | Ubuntu 14.04 LTS |
Python | 2.7.6 |
Thread使わない場合
まずはThreadなしでMainThread上でループ処理してるときにシグナルで終了するパターン。
#! /usr/bin/python # -*- coding: utf-8 -*- import logging import signal import time if __name__ == '__main__': logging.basicConfig( format='[%(threadName)s][%(levelname)s]%(message)s', level=logging.DEBUG ) logging.debug('-- START --') running = True def stop_handler(signum, frame): logging.debug('GET signal[%s]' % signum) global running running = False signal.signal(signal.SIGTERM, stop_handler) signal.signal(signal.SIGINT, stop_handler) cnt = 0 while running: logging.info('hoge[%s]' % cnt) cnt += 1 time.sleep(1) logging.debug('-- STOP --')
$ python sample1.py [MainThread][DEBUG]-- START -- [MainThread][INFO]hoge[0] [MainThread][INFO]hoge[1] [MainThread][INFO]hoge[2] [MainThread][INFO]hoge[3] ^C[MainThread][DEBUG]GET signal[2] [MainThread][DEBUG]-- STOP -- $
Ctrl+Cでシグナルキャッチしてループを抜けます。これはまあ余裕。
Thread使う場合
続いてはTheadを生成してそっちのThead上でループ処理してるときにシグナルで終了するパターン。
#! /usr/bin/python # -*- coding: utf-8 -*- import logging import signal import threading import time class Worker(threading.Thread): def __init__(self): self._running = True super(Worker, self).__init__() def run(self): cnt = 0 while self._running: logging.info('hoge[%s]' % cnt) cnt += 1 time.sleep(1) def shutdown(self): self._running = False if __name__ == '__main__': logging.basicConfig( format='[%(threadName)s][%(levelname)s]%(message)s', level=logging.DEBUG ) worker = Worker() logging.debug('-- START --') worker.start() def stop_handler(signum, frame): logging.debug('GET signal[%s]' % signum) worker.shutdown() signal.signal(signal.SIGTERM, stop_handler) signal.signal(signal.SIGINT, stop_handler) worker.join() logging.debug('-- STOP --')
$ python sample2.py [MainThread][DEBUG]-- START -- [Thread-1][INFO]hoge[0] [Thread-1][INFO]hoge[1] [Thread-1][INFO]hoge[2] ^C[Thread-1][INFO]hoge[3] ^C[Thread-1][INFO]hoge[4] [Thread-1][INFO]hoge[5] ^C[Thread-1][INFO]hoge[6] ^Z [1]+ 停止 python sample2.py
Ctrl+Cでシグナルキャッチできないのでアプリケーションが終わらない…。
何が問題なのかと言うと...
16.2. threading — 高水準のスレッドインタフェース — Python 2.7.14 ドキュメント
他のスレッドはスレッドの join() メソッドを呼び出せます。このメソッドは、 join() を呼び出されたスレッドが終了するまで、メソッドの呼び出し手となるスレッドをブロックします。
ということで、join()でスレッド終了待ちにしてしまうとシグナルを受け取るはずのMainThreadがブロックされるので受け取れないのである。しかも、シグナルを受け取れるのは...
17.4. signal — 非同期イベントにハンドラを設定する — Python 2.7.14 ドキュメント
主スレッドだけが新たなシグナルハンドラを設定することができ、したがってシグナルを受け取ることができるのは主スレッドだけです (これは、背後のスレッド実装が個々のスレッドに対するシグナル送信をサポートしているかに関わらず、 Python signal モジュールが強制している仕様です)。
Oh...
ちなみにPython3系だと
$ python3 sample2.py [MainThread][DEBUG]-- START -- [Thread-1][INFO]hoge[0] [Thread-1][INFO]hoge[1] [Thread-1][INFO]hoge[2] ^C[MainThread][DEBUG]GET signal[2] [MainThread][DEBUG]-- STOP --
Ctrl+Cでちゃんと抜けます。
Thread使う場合(正解)
--- sample2.py 2016-03-10 21:59:35.258517782 +0900 +++ sample2a.py 2016-03-10 22:07:21.526517782 +0900 @@ -42,5 +42,6 @@ signal.signal(signal.SIGTERM, stop_handler) signal.signal(signal.SIGINT, stop_handler) + signal.pause() worker.join() logging.debug('-- STOP --')
Threadクラスのjoin()でシグナル待ちをせずにsignal.pause()で待つのが正解。
$ python sample2a.py [MainThread][DEBUG]-- START -- [Thread-1][INFO]hoge[0] [Thread-1][INFO]hoge[1] ^C[MainThread][DEBUG]GET signal[2] [MainThread][DEBUG]-- STOP -- $ $ python3 sample2a.py [MainThread][DEBUG]-- START -- [Thread-1][INFO]hoge[0] [Thread-1][INFO]hoge[1] [Thread-1][INFO]hoge[2] ^C[MainThread][DEBUG]GET signal[2] [MainThread][DEBUG]-- STOP -- $
このようにPython2系でも3系でも動く。ということで、Python2系でスレッド終了待ちをする場合は気をつけましょう。
- 作者: Mark Lutz,夏目大
- 出版社/メーカー: オライリージャパン
- 発売日: 2009/02/26
- メディア: 大型本
- 購入: 12人 クリック: 423回
- この商品を含むブログ (133件) を見る