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つのスレッド t1
と t2
があります。スレッド t1
は counter
の値を 10 インクリメントしようとし、スレッド t2
は counter
の値を 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つのスレッド t1
と t2
を作成しました。両方のスレッドは同じ increase()
関数を対象としています。increase(by, lock)
関数は 2つのパラメーターを取ります。最初のパラメーターは counter
をインクリメントする量であり、2 番目のパラメーターは Lock
クラスのインスタンスです。以前の宣言に加えて、Python の threading
モジュール内に Lock
クラスのインスタンス lock
も作成しました。increase(by, lock)
関数のこの lock
パラメーターは、スレッドによって変更されている間、lock.acquire()
関数で counter
変数へのアクセスをロックし、lock でロックを解除します。1つのスレッドが
counter 変数を変更したときの release()
関数。スレッド t1
は counter
の値を 10 インクリメントし、スレッド t2
は counter
の値を 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
counter
を with lock:
ブロック内にインクリメントするためのコードを配置しました。スレッド t1
は counter
の値を 10 インクリメントし、スレッド t2
は counter
の値を 20 インクリメントします。競合状態は発生せず、counter
の最終値は 30 です。スレッドロックのロック解除について心配する必要はありません。
どちらの方法も完全に機能します。つまり、どちらの方法も競合状態の発生を防ぎますが、2 番目の方法は、スレッドロックのロックとロック解除に対処するという頭痛の種を防ぐため、最初の方法よりもはるかに優れています。また、最初の方法よりも書き込みがはるかにクリーンで、読み取りが簡単です。
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