初老のボケ防止日記

おっさんのひとりごとだから気にしないようにな。

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系でスレッド終了待ちをする場合は気をつけましょう。

初めてのPython 第3版

初めてのPython 第3版