Pythonでマルチスレッド(1) Event

はじめに

マルチスレッドプログラミングってサンプル読むだけだとわかった気になるだけで,自分で使いこなせるようにならないなーって実感している今日この頃.やっぱりいろいろ自分で実験するのが一番,ということでPythonマルチスレッドのあれこれを試していきたいと思います.

まずはEventからやっていきます.

準備

Python環境は3系を想定しています. 必要なモジュールのimportと,各スレッドの動きをわかりやすくするためにロガーの設定をします. ロガーはスレッド名や時刻も出力するようにしておくとGOOD.printとロガー出力は「まぜるな禁止」です(混ぜると出力の一貫性が崩れたりするらしい).

import logging
import threading
from threading import Thread
import queue
from time import sleep

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(threadName)s: %(message)s')

Event

threading --- スレッドベースの並列処理 — Python 3.7.5 ドキュメントの説明を見てみましょう.

イベントは、あるスレッドがイベントを発信し、他のスレッドはそれを待つという、スレッド間で通信を行うための最も単純なメカニズムの一つです。

イベントオブジェクトは内部フラグを管理します。このフラグは set() メソッドで値を true に、 clear() メソッドで値を false にリセットします。 wait() メソッドはフラグが true になるまでブロックします。

この後半部分を図にしてみました.これを頭の片隅に置きながら実験していきましょう.

f:id:weekend_warrior:20191120232247p:plain
Eventの内部フラグ変化の様子

Event.set()

まずはEventで起動しているスレッドを止めてみたいと思います. メインスレッドで別のスレッド(thread1)を起動し,一定時間(ここでは6秒)たったら停止させるというプログラムです. thread1ではwhileループで処理を継続させます.このループを抜けない限りはthread1スレッドは動き続けます.

ループの脱出条件に使っているのがEventです. Eventは初期状態ではFalseですが,セット(set())されるとis_set()Trueを返すようになるので,これを使ってループを脱出します.Eventはメインスレッドで定義し,セットします.

def thread1():
    logging.info('start')

    while not is_stopped.is_set():
        logging.info('...working...')
        sleep(2)

    logging.info('end')

if __name__ == '__main__':
    is_stopped = threading.Event()
    logging.info('is_stopped: ' + str(is_stopped.is_set()))

    t = Thread(target=thread1, name='thread1')
    t.start()
    sleep(6)

    logging.info('stopped thread1')
    is_stopped.set()
    logging.info('is_stopped: ' + str(is_stopped.is_set()))

結果

2019-11-10 22:04:41,995 MainThread: is_stopped: False  # (a)
2019-11-10 22:04:41,996 thread1: start
2019-11-10 22:04:41,996 thread1: ...working...
2019-11-10 22:04:43,997 thread1: ...working...
2019-11-10 22:04:46,002 thread1: ...working...
2019-11-10 22:04:47,997 MainThread: stopped thread1
2019-11-10 22:04:47,998 MainThread: is_stopped: True  # (b)
2019-11-10 22:04:48,007 thread1: end

メインスレッドにおけるEventのセットでthread1が停止したことがわかります. また,Eventはset()が実行されるまではis_set()Falseを返し(a),実行されるとTrueを返す(b)こともわかります.

Event.clear()

続いて,clear()です.set()と逆の動きになります. 下のコードではis_hard_modeというEventを用意して,set()clear()を繰り返します. 1秒おきに"...working..."か"...working so hard..."のメッセージが出力されますが,メッセージの内容がset()clear()で切り替わっていることがわかると思います.

def thread1():
    logging.info('start')

    while not is_stopped.is_set():
        if is_hard_mode.is_set():
            logging.info('...working so hard...')
        else:
            logging.info('...working...')
        sleep(1)

    logging.info('end')

if __name__ == '__main__':
    is_stopped = threading.Event()
    is_hard_mode = threading.Event()

    t = Thread(target=thread1, name='thread1')
    t.start()

    sleep(3)
    is_hard_mode.set()
    sleep(2)
    is_hard_mode.clear()
    sleep(2)
    is_hard_mode.set()
    sleep(3)

    is_stopped.set()

結果

2019-11-10 22:57:01,789 thread1: start
2019-11-10 22:57:01,789 thread1: ...working...
2019-11-10 22:57:02,794 thread1: ...working...
2019-11-10 22:57:03,799 thread1: ...working...
2019-11-10 22:57:04,803 thread1: ...working so hard...
2019-11-10 22:57:05,807 thread1: ...working so hard...
2019-11-10 22:57:06,809 thread1: ...working...
2019-11-10 22:57:07,815 thread1: ...working...
2019-11-10 22:57:08,820 thread1: ...working so hard...
2019-11-10 22:57:09,820 thread1: ...working so hard...
2019-11-10 22:57:10,826 thread1: ...working so hard...
2019-11-10 22:57:11,829 thread1: end

Event.wait()

Eventがset()されるまで,そこでスレッドの進行をブロック(一時停止)します. thread1スレッドが起動されると,(a)まで処理が進みます. ここでis_startedwait()が呼び出されます(b).すると,is_startedset()されるまでこのスレッドは待つことになります. is_startedset()されているのはメインスレッドで,thread1スレッドを起動してから3秒後です.

def thread1():
    logging.info('waiting')
    logging.info('is_started: ' + str(is_started.is_set()))  # (a)

    is_started.wait()  # (b)

    logging.info('is_started: ' + str(is_started.is_set()))  # (d)
    logging.info('start')


if __name__ == '__main__':
    logging.info('main start')
    is_started = threading.Event()
    t = Thread(target=thread1, name='thread1')
    t.start()

    sleep(3)
    is_started.set()  # (c)

    t.join()

結果

結果を見てみると,(a)と(d)の間で3秒経過していることがわかります.また,is_started.is_set()FalseからTrueに変わっていることが確認できます.

2019-11-12 22:28:25,257 MainThread: main start
2019-11-12 22:28:25,257 thread1: waiting
2019-11-12 22:28:25,257 thread1: is_started: False
2019-11-12 22:28:28,259 thread1: is_started: True
2019-11-12 22:28:28,259 thread1: start