Python スレッドロック徹底解説: `thread.LockType` から高機能同期機構まで


本記事では、thread.LockTypeの役割、種類、使用方法について分かりやすく解説します。

スレッドロックの必要性

Pythonは、CPython実装において**グローバル インタプリター ロック (GIL)**と呼ばれる制限があります。これは、一度に1つのスレッドしかPythonバイトコードを実行できないことを意味します。しかし、複数のスレッドが共有リソースにアクセスする場合、データ競合が発生する可能性があります。

データ競合とは、複数のスレッドが同じデータを同時に読み書きしようとする状況で、予期しない結果が生じる可能性がある問題です。例えば、銀行口座の残高を2つのスレッドが同時に引き出そうとすると、残高が正しく更新されない可能性があります。

このような問題を解決するために、スレッドロック機構が用いられます。スレッドロックは、共有リソースへのアクセスを排他的に制御し、データ競合を防ぎます。

thread.LockTypeクラスは、スレッドロックオブジェクトを作成するためのクラスです。このクラスを用いることで、共有リソースへのアクセスを排他的に制御することができます。

thread.LockTypeオブジェクトには、以下のメソッドが用意されています。

  • locked(): ロックが取得されているかどうかを確認します。
  • release(): ロックを解放します。
  • acquire(): ロックを獲得します。ロックがすでに取得されている場合は、ブロックされるまで待機します。

thread.LockType の種類

thread.LockType には、以下の2種類があります。

  • Semaphore: 特定数のロックを同時に許可することができます。指定された数のロックがすべて取得されている場合は、他のスレッドはブロックされます。
  • RLock (再入可能ロック): 同じスレッドが複数回ロックを獲得することができます。再入回数をカウントし、ロックを解除するまでカウントを維持します。

以下に、thread.LockType の基本的な使用方法を示します。

from threading import Lock

# 共有リソース
counter = 0

# ロックオブジェクトの作成
lock = Lock()

def increment_counter():
    global counter

    # ロックを獲得
    with lock:
        # クリティカルセクション
        counter += 1

# スレッドの作成と実行
thread1 = threading.Thread(target=increment_counter)
thread2 = threading.Thread(target=increment_counter)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

# カウンターの値を確認
print(counter)

この例では、2つのスレッドがcounter変数を同時にインクリメントしようとしています。Lockオブジェクトを使用することで、2つのスレッドがクリティカルセクションに同時にアクセスしないように制御しています。

  • thread.LockTypeは、CPythonでのみ利用可能です。他のPython実装では異なる同期機構が提供されている場合があります。
  • thread.LockTypeは、低レベルのスレッド同期機構です。より高レベルな同期機構として、threadingモジュールのLockSemaphoreクラスを使用することを推奨します。

thread.LockTypeクラスは、Pythonにおけるスレッドロックの基礎的な概念を理解する上で重要です。スレッド競合を防ぎ、データ整合性を保つために、適切なスレッドロック機構を使用することが重要です。



シミュレートされた銀行口座

この例では、複数のスレッドが銀行口座から同時に出金を行うシナリオをシミュレートします。Accountクラスは、預金残高とロックオブジェクトを持つ銀行口座を表します。

from threading import Lock

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

    def withdraw(self, amount):
        with self.lock:
            if self.balance >= amount:
                self.balance -= amount
                print(f"Withdrawal of {amount} successful. Remaining balance: {self.balance}")
            else:
                print(f"Insufficient funds. Current balance: {self.balance}")

出金スレッド

withdraw_from_account 関数は、Account オブジェクトからランダムな金額を引き出すスレッドを表します。

def withdraw_from_account(account):
    amount = random.randint(10, 50)
    account.withdraw(amount)

メインプログラムは、Account オブジェクトを作成し、複数のスレッドを起動して出金を行います。

if __name__ == "__main__":
    account = Account(1000)

    num_threads = 10
    threads = []

    for _ in range(num_threads):
        thread = threading.Thread(target=withdraw_from_account, args=(account,))
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()

このプログラムを実行すると、複数のスレッドがAccount オブジェクトから同時に出金を行います。Lock オブジェクトを使用することで、各スレッドが排他的に口座残高にアクセスし、データ競合を防ぐことができます。

この例以外にも、thread.LockType を使用してさまざまな同期処理を実装することができます。例えば、以下のような例が考えられます。

  • プリンタなどの共有デバイスへのアクセス
  • ファイルへの読み書き
  • 共有リストへの要素の追加と削除


threadingモジュールの同期機構

threadingモジュールには、LockSemaphoreなどの高レベルな同期機構が提供されています。これらの同期機構は、thread.LockTypeよりも使いやすく、多くの場合で十分な機能を提供します。

  • Condition: 特定条件を満たすまでスレッドをブロックする同期機構です。
  • Event: スレッド間のイベント通知に使用されます。
  • Semaphore: 特定数のロックを同時に許可する同期機構です。thread.LockTypeSemaphoreと同様の機能を提供します。
  • Lock: 再入可能な排他ロックです。thread.LockTypeRLockと同様の機能を提供します。

状況によっては、threadingモジュールの同期機構よりも高度な同期機構が必要になる場合があります。以下に、いくつかの例を示します。

  • サードパーティ製の同期ライブラリ: より高度な機能を提供するサードパーティ製の同期ライブラリも多数存在します。
  • concurrent.futuresモジュールのThreadPoolExecutor: スレッドプールを使用してタスクを並行実行するためのクラスです。
  • queueモジュールのQueue: スレッド間で安全にデータを共有するためのキューです。

thread.LockTypeを使用する際の注意点

thread.LockTypeを使用する場合は、以下の点に注意する必要があります。

  • thread.LockTypeは、デッドロックが発生しやすいという問題があります。デッドロックを防ぐためには、適切なロック順序を使用する必要があります。
  • thread.LockTypeは、CPythonでのみ利用可能です。他のPython実装では異なる同期機構が提供されている場合があります。