PythonにおけるGILの概念と解放タイミング、およびミューテックスロックとの違い

  1. GILとは何か

GIL(Global Interpreter Lock)はPythonインタープリタに実装されたグローバルロックであり、単一プロセス内で複数のスレッドが存在する場合、あるスレッドがPythonインタープリタを独占的に利用するための制御機構です。このロックにより、同一プロセス内の他のスレッドは実行できず、対象スレッドが処理を完了するまで待機します。ただし、I/O処理などの時間のかかる操作を行う際にはロックが解放され、他のスレッドが実行可能になります。従って、マルチスレッド環境でも実際には並列実行ではなく順次実行となります。

GILは「アクセス許可証」と考えることができ、Pythonプロセス内では唯一の存在です。許可証を取得できないスレッドはCPUで実行できません。この仕組みはCPython実装に特有のものです。

  1. GILロックの解放タイミング

  2. あるスレッドが処理を完了すると、他のスレッドが実行可能になる

  3. スレッドが時間のかかる処理(I/Oなど)に遭遇した際にロックが解放され、他のスレッドが実行できるようになる

  4. ミューテックスロックとGILの違い

  5. 例:マルチスレッドによるデータ破損問題

import threading

shared_counter = 0

def increment_operation():
    global shared_counter
    
    for iteration in range(1000000):
        shared_counter += 1
    print(f'スレッド{threading.current_thread().name}の結果:', shared_counter)

if __name__ == '__main__':
    worker_threads = []
    for index in range(10):
        thread_instance = threading.Thread(target=increment_operation)
        thread_instance.start()
        worker_threads.append(thread_instance)

    for thread_instance in worker_threads:
        thread_instance.join()
    print('メインスレッド終了')

実行結果:データが明らかに破損している

スレッドThread-1の結果: 1000000
スレッドThread-2の結果:スレッドThread-3の結果: 1581377
 スレッドThread-4の結果: 2690126
 スレッドThread-5の結果: 35138763690126

スレッドThread-6の結果: スレッドThread-7の結果: 5545936
4690126
スレッドThread-8の結果:スレッドThread-9の結果:  6244936
スレッドThread-10の結果: 82094017209401

メインスレッド終了

Process finished with exit code 0
  1. データ破損の原因:

CPUを複数の時間スライスに分割し、10スレッドを起動して10個のCPU時間スライスを割り当てます。カウント値が小さい場合、単一のCPU時間スライス内でforループが完了するためデータ破損は発生しません。しかしカウント値が大きい場合、単一の時間スライス内でループが完了せず、連続した複数のCPU時間スライスが割り当てられないため、あるスレッドが完全に終了する前に次のスレッドが実行開始します。

この問題の根本的な原因は、複数スレッドによる同一リソースへのアクセス制御が行われず、データが破壊されることによりスレッドの実行結果が予測不能になることです。このような現象を「スレッドアンセーフ」と呼びます。

  1. データ破損の解決(同期ロック、ミューテックス)

mutex_lock = Lock():同期ロックの作成 mutex_lock.acquire():ロックの獲得 mutex_lock.release():ロックの解放;解放しない場合、他のスレッドはロックを取得できず実行できません 解放後、他のスレッドがロックを競合します。誰が先に取得するかは不定 同じロックインスタンスを使用しなければなりません

import threading


shared_counter = 0

def increment_operation():
    global shared_counter
    
    mutex_lock.acquire()
    for iteration in range(1000000):
        shared_counter += 1
    print(f'スレッド{threading.current_thread().name}の結果:', shared_counter)
    mutex_lock.release()

if __name__ == '__main__':
    mutex_lock = threading.Lock()
    worker_threads = []
    for index in range(10):
        thread_instance = threading.Thread(target=increment_operation)
        thread_instance.start()
        worker_threads.append(thread_instance)

    for thread_instance in worker_threads:
        thread_instance.join()
    print('メインスレッド終了')

実行結果:

スレッドThread-1の結果: 1000000
スレッドThread-2の結果: 2000000
スレッドThread-3の結果: 3000000
スレッドThread-4の結果: 4000000
スレッドThread-5の結果: 5000000
スレッドThread-6の結果: 6000000
スレッドThread-7の結果: 7000000
スレッドThread-8の結果: 8000000
スレッドThread-9の結果: 9000000
スレッドThread-10の結果: 10000000
メインスレッド終了
  1. まとめ:

マルチスレッドプログラミングにおいて、threadingモジュールのLock関数を呼び出してミューテックスロックを取得します。ミューテックスロックは共有データに対するロックを行い、同時刻に一つのスレッドのみがデータを操作することを保証し、データレベルのロックです。

GILロックはインタープリタレベルのロックであり、同時刻にプロセス内で一つのスレッドのみがGILを保持し、実行権限を持つことを保証します。

タグ: Python GIL Multithreading mutex thread-safety

6月28日 22:24 投稿