Pythonには複数のインタプリタ(Interpreter)が存在します。例えば、CPython(Cで実装)、Jython(Javaで実装)、IronPython(C#で実装)、PyPy(RPythonで実装)などがあります。
PythonのデフォルトのインタプリタはCPythonです。CPythonには GIL(Global Interpreter Lock) と呼ばれるロックが存在します。
このロックの役割は、同時に実行できるPythonのバイトコードを1つのスレッドのみに制限することです(たとえCPUがマルチコアであっても)。
では、なぜGILが存在するのでしょうか。
これを理解するためには、まずPythonのメモリ管理の仕組みについて説明する必要があります。
Pythonのメモリ管理は主に 参照カウント(reference counting) を使用しています。
システムはそれぞれのPythonオブジェクトが何回参照されているかを追跡し、あるオブジェクトの参照回数が 0 になると、そのオブジェクトが占有しているメモリは解放されます。
しかし、CPythonにおいて Pythonオブジェクトの参照カウントを更新する処理はスレッドセーフではありません。そのため、race condition の問題が発生する可能性があります。
複数のスレッドが同時に参照カウントを更新し、しかもそれを保護するロックが存在しない場合、カウント結果が誤ってしまう可能性があります。その結果、以下のような問題が発生する可能性があります。
- メモリリーク(memory leak)が発生し、メモリが解放されなくなる
- まだ参照が存在しているのにメモリが解放されてしまって、Pythonプログラムがクラッシュしたり、様々な予想外のエラーが発生したりします
参照カウント更新時に発生する可能性のあるrace conditionを解決するためのある直感的な方法は、参照カウントを更新する処理にロックを追加することです。
しかし、ほとんどすべてのPythonオブジェクト操作は参照カウントの変更を伴う可能性があります。そのため、race conditionが発生する可能性のあるすべての箇所でロックを使用すると、以下の問題が発生します。
- デッドロックのリスクが増える
- ロックの取得と解放が非常に頻繁に発生して、パフォーマンスが低下になる
- ロック管理が複雑になる
そのため、当時のCPythonの開発者は、インタプリタ自体にグローバルロックを導入するという方法を選択しました。これが GIL(Global Interpreter Lock) です。そして、Pythonのバイトコードを実行するすべてのスレッドは、まずGILを取得しなければならないというルールが定められました。
注意すべき点として、GILの設計により、CPUバウンドな処理ではマルチスレッドを使っても実際には加速させることができません。むしろ、GILの取得と解放によるオーバーヘッドによって、処理速度が遅くなる場合もあります。
そのため、CPUバウンドな処理とCPythonの場合で加速させたいときは、マルチプロセス(multiprocessing)を使用してください。
参考文献:
- 【python】天使还是魔鬼?GIL的前世今生。一期视频全面了解GIL! by 码农高天
- What Is the Python Global Interpreter Lock (GIL)? by Abhinav Ajitsaria