Python でのスレッドロック

Muhammad Maisam Abbas 2023年1月30日
  1. Python での競合状態
  2. Python でのスレッドロック
  3. Python で with lock:を使用したスレッドロック
Python でのスレッドロック

このチュートリアルでは、Python でスレッドロックを利用するためのさまざまな方法について説明します。

Python での競合状態

競合状態は、複数のスレッドが同じ共有変数を変更しようとしたときに発生する問題です。すべてのスレッドは、共有変数から同時に同じ値を読み取ります。次に、すべてのスレッドが共有変数の値を変更しようとします。ただし、変数は前のスレッドによって書き込まれた値を上書きするため、最後のスレッドの値を格納するだけになります。この意味で、すべてのスレッド間で競合が発生し、最終的にどのスレッドが変数の値を変更するかを確認します。この現象は、次のコードの例で示されています。

from threading import Thread

counter = 0


def increase(by):
    global counter
    local_counter = counter
    local_counter += by
    counter = local_counter
    print(f"counter={counter}")


t1 = Thread(target=increase, args=(10,))
t2 = Thread(target=increase, args=(20,))

t1.start()
t2.start()

t1.join()
t2.join()

print(f"The final counter is {counter}")

出力:

counter=10
counter=20
The final counter is 20

グローバル共有変数 counter = 0 と 2つのスレッド t1t2 があります。スレッド t1counter の値を 10 インクリメントしようとし、スレッド t2counter の値を 20 インクリメントしようとします。上記のコードでは、両方のスレッドを同時に実行し、counter の値を変更しようとします。上記のロジックにより、counter の最終値は 30 になります。ただし、競合状態のため、counter は 10 または 20 になります。

Python でのスレッドロック

スレッドロックは、競合状態を防ぐために使用されます。スレッドロックは、あるスレッドが共有変数にアクセスできるようにロックし、他のスレッドが共有変数にアクセスできないようにします。次に、スレッドが共有変数を使用していないときにロックを解除して、変数を他のスレッドが処理できるようにします。スレッドモジュール内の Lock クラスは、Python でスレッドロックを作成するために使用されます。acquire() メソッドは共有変数へのアクセスをロックするために使用され、release() メソッドはロックのロックを解除するために使用されます。release() メソッドは、ロックされていないロックで使用された場合、RuntimeError 例外をスローします。

from threading import Thread, Lock

counter = 0


def increase(by, lock):
    global counter

    lock.acquire()

    local_counter = counter
    local_counter += by
    counter = local_counter
    print(f"counter={counter}")

    lock.release()


lock = Lock()

t1 = Thread(target=increase, args=(10, lock))
t2 = Thread(target=increase, args=(20, lock))

t1.start()
t2.start()

t1.join()
t2.join()

print(f"The final counter is {counter}")

出力:

counter=10
counter=30
The final counter is 30

グローバルに共有される変数 counter=0 と 2つのスレッド t1t2 を作成しました。両方のスレッドは同じ increase() 関数を対象としています。increase(by, lock) 関数は 2つのパラメーターを取ります。最初のパラメーターは counter をインクリメントする量であり、2 番目のパラメーターは Lock クラスのインスタンスです。以前の宣言に加えて、Python の threading モジュール内に Lock クラスのインスタンス lock も作成しました。increase(by, lock) 関数のこの lock パラメーターは、スレッドによって変更されている間、lock.acquire() 関数で counter 変数へのアクセスをロックし、lock でロックを解除します。1つのスレッドが counter 変数を変更したときの release() 関数。スレッド t1counter の値を 10 インクリメントし、スレッド t2counter の値を 20 インクリメントします。

スレッドロックのため、競合状態は発生せず、counter の最終値は 30 です。

Python で with lock:を使用したスレッドロック

前の方法の問題は、1つのスレッドが処理を完了したときに、ロックされた各変数のロックを慎重に解除する必要があることです。正しく行われなかった場合、共有変数は最初のスレッドによってのみアクセスされ、他のスレッドは共有変数へのアクセスを許可されません。この問題は、コンテキスト管理を使用することで回避できます。with lock:を使用して、すべての重要なコードをこのブロック内に配置できます。これは、競合状態を防ぐためのはるかに簡単な方法です。次のコードスニペットは、Python での競合状態を防ぐための with lock:の使用を示しています。

from threading import Thread, Lock

counter = 0


def increase(by, lock):
    global counter

    with lock:
        local_counter = counter
        local_counter += by
        counter = local_counter
    print(f"counter={counter}")


lock = Lock()

t1 = Thread(target=increase, args=(10, lock))
t2 = Thread(target=increase, args=(20, lock))

t1.start()
t2.start()

t1.join()
t2.join()

print(f"The final counter is {counter}")

出力:

counter=10
counter=30
The final counter is 30

counterwith lock:ブロック内にインクリメントするためのコードを配置しました。スレッド t1counter の値を 10 インクリメントし、スレッド t2counter の値を 20 インクリメントします。競合状態は発生せず、counter の最終値は 30 です。スレッドロックのロック解除について心配する必要はありません。

どちらの方法も完全に機能します。つまり、どちらの方法も競合状態の発生を防ぎますが、2 番目の方法は、スレッドロックのロックとロック解除に対処するという頭痛の種を防ぐため、最初の方法よりもはるかに優れています。また、最初の方法よりも書き込みがはるかにクリーンで、読み取りが簡単です。

Muhammad Maisam Abbas avatar Muhammad Maisam Abbas avatar

Maisam is a highly skilled and motivated Data Scientist. He has over 4 years of experience with Python programming language. He loves solving complex problems and sharing his results on the internet.

LinkedIn

関連記事 - Python Thread