Python の RLock で再入可能なロックを使う

RLock(Reentrant Lock、再入可能ロック)は、同じスレッドから複数回取得できるロックです。通常の Lock では同じスレッドが2回ロックを取得しようとするとデッドロックになりますが、RLock ではそれを避けられます。

Lock と RLock の違い

通常の Lock は、同じスレッドであっても2回取得しようとするとブロックされます。

import threading

lock = threading.Lock()

def outer():
    with lock:
        print("outer")
        inner()  # ここでデッドロック!

def inner():
    with lock:  # 同じスレッドでも再取得できない
        print("inner")

# outer()  # これを実行するとデッドロック

RLock を使えば、同じスレッドから何度でもロックを取得できます。

import threading

rlock = threading.RLock()

def outer():
    with rlock:
        print("outer")
        inner()

def inner():
    with rlock:  # 同じスレッドなので取得できる
        print("inner")

outer()
# outer
# inner

RLock の動作

Lock

同じスレッドでも2回目の acquire でブロックされる。

RLock

同じスレッドなら何度でも acquire できる。acquire と同じ回数だけ release が必要。

RLock は内部でカウンターを持っており、acquire() の回数と同じだけ release() を呼ぶ必要があります。

import threading

rlock = threading.RLock()

rlock.acquire()
print("1回目取得")
rlock.acquire()
print("2回目取得")
rlock.acquire()
print("3回目取得")

rlock.release()
print("1回目解放")
rlock.release()
print("2回目解放")
rlock.release()
print("3回目解放(完全に解放)")

実用例:再帰的な処理

再帰的にロックを必要とする処理で RLock が役立ちます。

import threading

class TreeNode:
    def __init__(self, value):
        self.value = value
        self.children = []
        self._lock = threading.RLock()
    
    def add_child(self, node):
        with self._lock:
            self.children.append(node)
    
    def total(self):
        with self._lock:
            total = self.value
            for child in self.children:
                total += child.total()  # 再帰的に呼び出し
            return total

root = TreeNode(10)
root.add_child(TreeNode(5))
root.add_child(TreeNode(3))
print(root.total())  # 18

実用例:メソッド間でロックを共有

複数のメソッドが同じロックを使い、互いに呼び出し合う場合に便利です。

import threading

class BankAccount:
    def __init__(self, balance):
        self.balance = balance
        self._lock = threading.RLock()
    
    def deposit(self, amount):
        with self._lock:
            self.balance += amount
            print(f"入金: {amount}, 残高: {self.balance}")
    
    def withdraw(self, amount):
        with self._lock:
            if self.balance >= amount:
                self.balance -= amount
                print(f"出金: {amount}, 残高: {self.balance}")
                return True
            return False
    
    def transfer(self, other, amount):
        with self._lock:
            if self.withdraw(amount):  # withdraw も同じロックを使う
                other.deposit(amount)

account1 = BankAccount(1000)
account2 = BankAccount(500)
account1.transfer(account2, 200)

いつ RLock を使うべきか

RLock が必要なケース

同じロックで保護されたメソッドを再帰的に呼び出す場合。メソッドが他のロック保護されたメソッドを呼ぶ場合。

Lock で十分なケース

単純な排他制御。再入の必要がない場合。パフォーマンスを重視する場合(Lock の方がわずかに高速)。

RLock は便利ですが、必要ない場合は通常の Lock を使う方がシンプルです。デッドロックのリスクがある場合にのみ RLock を検討しましょう。