Pythonのスレッドをロックする

Python学習【365日チャレンジ!】209日目のマスターU(@Udemy11)です。

家の中を整理したので、かなりの不用品が出てきました。

全く使っていない新品の食器などがたくさん出てきたので、メルカリで売ることにしたのですが、ちょっとお得な価格で出品するとほんとあっという間に売れちゃうんです。

コマーシャルでも売れている商品の90%は出品から24時間以内に売れているなんてことを言っていたので、ほんとめちゃくちゃたくさんの人がメルカリをチェックしてるんでしょうね。

ちなみによく売れている商品だったとしても値段がちょっと高いと売れないんです。

それでは、今日もPython学習をはじめましょう。

昨日の復習

昨日は、スレッドのTimerオブジェクトについて学習しました。

スレッドの処理を後回しにしたいときに使うのがTimerオブジェクトでした。

argskwargsの引数をターゲットの関数に引き渡すこともできましたね。

詳細については、昨日の記事をごらんください

今日は、同時進行の処理を受け付けなくするLockを学習します。

thread.py

今日は、Lockの学習をしますが、そのまえに、どのような場面で使うかを理解しておきましょう。

次のような、辞書型の値を使うスレッドについて考えます。

import logging
import threading


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


def thread1(d):
    logging.debug('start1')
    i = d['x']
    d['x'] = i + 1
    logging.debug(d)
    logging.debug('end1')

def thread2(d):
    logging.debug('start2')
    i = d['x']
    d['x'] = i + 1
    logging.debug(d)
    logging.debug('end2')

if __name__ == '__main__':
    d = {'x': 1}
    t1 = threading.Thread(target=thread1, args=(d, ))
    t2 = threading.Thread(target=thread2, args=(d, ))
    t1.start()
    t2.start()

ハイライトしている行が変更したところですが、辞書型の引数dを取るスレッドを2つ用意して、同じ計算をさせています。

このコードを実行すると、次のようになります。

実行結果

Thread-1: start1
Thread-1: {'x': 2}
Thread-1: end1
Thread-2: start2
Thread-2: {'x': 3}
Thread-2: end2

thread2では、d1が代入されるのではなく、thread1の計算結果である2が代入されて計算されています。

これは、並列処理から考えると少し変な感じがしますが、上から順番に処理されるルールになっているのかなとも思います。

次に、11行目にtime.sleep(1)を挿入して、thread1の処理を1秒間遅らせるとどうなるでしょう。

実行結果は次のようになります。

実行結果

Thread-1: start1
Thread-1: {'x': 2}
Thread-1: end1
Thread-2: start2
Thread-2: {'x': 2}
Thread-2: end2

この場合は、thread2dに1が代入されて上書きされるので、xが同じ2になってしまいます。

ただ、thread2の18行目にtime.sleep(1)を代入した場合は、最初の出力結果と同じになるんですよね。

なぜ同じタイムリープを入れてもちがう結果になるのかはちょっと理解できていないのですが、この対処法として、Lockを使う方法があります。

requireとrelease

インポートなどの行は省いて8行目から記述しますが、ハイライトしている行が変更部分です。

def thread1(d, lock):
    logging.debug('start1')
    lock.acquire()
    i = d['x']
    time.sleep(1)
    d['x'] = i + 1
    logging.debug(d)
    lock.release()
    logging.debug('end1')

def thread2(d, lock):
    logging.debug('start2')
    lock.acquire()
    i = d['x']
    d['x'] = i + 1
    logging.debug(d)
    lock.release()
    logging.debug('end2')

if __name__ == '__main__':
    d = {'x': 1}
    lock = threading.Lock()
    t1 = threading.Thread(target=thread1, args=(d, lock))
    t2 = threading.Thread(target=thread2, args=(d, lock))
    t1.start()
    t2.start()

29行目でLockオブジェクトを作成し、8行目、18行目の関数と30行目・31行目のオブジェクトに引数のlockを入れて、それぞれの関数の計算が始まる前にlock.require()と出力が終わったあとにlock.release()を挿入しています。

これで実行結果は最初と同じようになります。

実行結果

Thread-1: start1
Thread-2: start2
Thread-1: {'x': 2}
Thread-1: end1
Thread-2: {'x': 3}
Thread-2: end2

バグの発見につながる

今日学習したコードは、処理方法やタイミングによって実行結果が違ってくるので、バグにつながるコードにも関わらず、普通に問題なく出力していると問題に気づくことがありません。

このようなバグを見つけ出すためにテストを行っているわけなので、同じ処理なのに実行結果が違ってくる場合は、原因究明が必要です。

地道な作業がプログラムのスキルを向上させる近道ではあるので、いろいろとコードを試すようにしましょう。

それでは明日もGood Python!