【実践解説】Pythonマルチスレッドで `threading.Lock.release()` を使って銀行口座の残高を安全に更新する方法


Python の threading モジュールは、マルチスレッドプログラミングを可能にする機能を提供します。マルチスレッドとは、複数のタスクを同時に実行する処理方法です。

マルチスレッドプログラミングでは、複数のスレッドが共有リソースにアクセスする可能性があります。競合状態を避けるために、threading.Lock などの同期オブジェクトを使用する必要があります。

threading.Lock.release() の役割

threading.Lock オブジェクトは、共有リソースへの排他アクセスを制御するために使用されます。acquire() メソッドを使用してロックを取得し、release() メソッドを使用してロックを解放します。

release() メソッドは、現在ロックを保持しているスレッドによって呼び出される必要があります。ロックが解放されると、他のスレッドがロックを取得できるようになります。

例:共有カウンタの更新

以下の例では、threading.Lock オブジェクトを使用して、共有カウンタの値を安全に更新する方法を示します。

import threading

class Counter:
    def __init__(self):
        self.count = 0
        self.lock = threading.Lock()

    def increment(self):
        with self.lock:
            self.count += 1

counter = Counter()

def worker():
    for _ in range(1000):
        counter.increment()

threads = []
for _ in range(4):
    thread = threading.Thread(target=worker)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(counter.count)

この例では、4つのスレッドが Counter オブジェクトの increment() メソッドを同時に呼び出します。increment() メソッドは lock オブジェクトを使用して排他アクセスを制御するため、カウンタ値が正しく更新されます。

with ステートメントの使用

with ステートメントを使用すると、acquire()release() メソッドを明示的に呼び出すことなく、ロックを自動的に取得および解放できます。

import threading

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        with self.lock:
            self.count += 1

counter = Counter()

def worker():
    for _ in range(1000):
        counter.increment()

threads = []
for _ in range(4):
    thread = threading.Thread(target=worker)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(counter.count)

この例は、上の例と同じ動作ですが、with ステートメントを使用してコードを簡潔にしています。



共有カウンタの更新

import threading

class Counter:
    def __init__(self):
        self.count = 0
        self.lock = threading.Lock()

    def increment(self):
        with self.lock:
            self.count += 1

counter = Counter()

def worker():
    for _ in range(1000):
        counter.increment()

threads = []
for _ in range(4):
    thread = threading.Thread(target=worker)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(counter.count)

説明

  • メインスレッドは、カウンタの最終値を出力します。
  • メインスレッドは、すべてのワーカー スレッドが完了するのを待機します。
  • メインスレッドは、4 つのワーカー スレッドを作成して開始します。
  • worker() 関数は、increment() メソッドを 1000 回呼び出します。
  • with ステートメントは、lock オブジェクトを自動的に取得および解放します。
  • increment() メソッドは、カウンタ値を 1 ずつ増加させます。
  • __init__() メソッドは、カウンタとロックオブジェクトを初期化します。
  • Counter クラスは、共有カウンタを表します。

この例では、threading.Lock オブジェクトを使用して、銀行口座の残高を安全に更新する方法を示します。

import threading

class BankAccount:
    def __init__(self, initial_balance):
        self.balance = initial_balance
        self.lock = threading.Lock()

    def withdraw(self, amount):
        with self.lock:
            if self.balance >= amount:
                self.balance -= amount
                return True
            else:
                return False

    def deposit(self, amount):
        with self.lock:
            self.balance += amount

account = BankAccount(1000)

def withdraw_money(amount):
    if account.withdraw(amount):
        print(f"引き出しに成功しました: {amount}")
    else:
        print(f"引き出しに失敗しました: 残高不足")

def deposit_money(amount):
    account.deposit(amount)
    print(f"入金に成功しました: {amount}")

threads = []

# 引き出しスレッド
thread1 = threading.Thread(target=withdraw_money, args=(500,))
threads.append(thread1)

# 入金スレッド
thread2 = threading.Thread(target=deposit_money, args=(700,))
threads.append(thread2)

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print(f"現在の残高: {account.balance}")
  • メインスレッドは、口座の現在の残高を出力します。
  • メインスレッドは、すべてのスレッドが完了するのを待機します。
  • メインスレッドは、withdraw_money()deposit_money() 関数をそれぞれ実行するスレッドを作成します。
  • deposit_money() 関数は、deposit() メソッドを呼び出して口座に 700 ドルを入金します。
  • withdraw_money() 関数は、withdraw() メソッドを呼び出して口座から 500 ドルを引き出します。
  • deposit() メソッドは、口座に入金します。
  • withdraw() メソッドは、口座から指定金額を引き出します。
  • __init__() メソッドは、残高とロックオブジェクトを初期化します。
  • BankAccount クラスは、銀行口座を表します。


以下に、threading.Lock.release() の代替方法のいくつかと、それぞれの利点と欠点について説明します。

セマフォ

セマフォは、共有リソースの使用を制限するために使用される同期プリミティブです。リソースの使用許可が与えられたスレッドの数に制限を設けることができます。

利点

  • 優先順位制御を容易に実装できる。
  • ロックよりも柔軟性が高い。

欠点

  • デッドロックが発生する可能性がある。
  • ロックよりも複雑。


import threading

class ResourcePool:
    def __init__(self, max_resources):
        self.semaphore = threading.Semaphore(max_resources)

    def acquire(self):
        self.semaphore.acquire()

    def release(self):
        self.semaphore.release()

pool = ResourcePool(5)

def worker():
    with pool:
        # 共有リソースを使用する
        pass

threads = []
for _ in range(10):
    thread = threading.Thread(target=worker)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

イベント

イベントは、スレッド間の非同期通信に使用される同期プリミティブです。スレッドは、イベントがシグナルされるのを待機してから、処理を続行できます。

利点

  • スレッド間の非同期通信に適している。
  • シンプルで使いやすい。

欠点

  • 排他アクセス制御には使用できない。


import threading

event = threading.Event()

def worker():
    event.wait()
    # 共有リソースを使用する
    pass

event.set()

threads = []
for _ in range(10):
    thread = threading.Thread(target=worker)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

RLock

RLock は、再帰ロックとも呼ばれ、スレッドがロックを複数回取得できるようにする同期プリミティブです。これは、再帰的なデータ構造を処理する必要がある場合に役立ちます。

利点

  • デッドロックが発生する可能性が低い。
  • 再帰的なデータ構造の処理に適している。

欠点

  • ロックよりも複雑。


import threading

class TreeNode:
    def __init__(self, value):
        self.value = value
        self.lock = threading.RLock()

        self.left = None
        self.right = None

    def add_child(self, child):
        with self.lock:
            if child.value < self.value:
                self.left = child
            else:
                self.right = child

root = TreeNode(10)
root.add_child(TreeNode(5))
root.add_child(TreeNode(15))

上記以外にも、threading.Lock.release() の代替方法として、以下のようなものがあります。

  • メッセージパッシング
  • アトミック操作
  • ダブルチェックロック

どの代替方法が最適かは、具体的な状況によって異なります。