PythonのGILノート
GILとは
GILはGlobal Interpreter Lockの略で、Python解釈器を1つのスレッドで制御できる反発ロックです.つまり、同じ時間に1つのスレッドしか実行できないということです.複数のスレッドが同時に起動しても、並列実行ではありません.このGILは単一スレッドプログラムには影響がなく,マルチスレッドプログラムには影響が大きいが,CPU密集型プログラムではマルチスレッドを起動するか起動しないかで性能に差がないので,後でコードで検証する.
GILのPythonでの用途
Pythonは参照カウントを使用してメモリ管理を行うことが知られています.Pythonで作成されたオブジェクトには、オブジェクトへの参照の数を追跡する参照カウント変数があります.このカウントがゼロに達すると、オブジェクトが消費するメモリが解放されます.
上の例のように、aとsys.getrefcountは2回参照しているので、出力参照回数は2で、直接sysであれば.getrefcount([])はsysのみです.getrefcountの1回の参照は、b=aであれば、もう一度参照を計算するのでsys.getrefcount(a)出力は3回の参照であるが、再びsys.getrefcount(a)はなぜ3なのか.そうじゃないgetrefcountも1回の参照ですか?これは、aが変数としてsysに伝達するためである.getrefcountでは確かに1回の参照を計算しますが、この関数の実行が完了すると、この参照も減ってしまうので、この関数を何度も実行しても同じように見えます.a=-5~256の場合、Pythonが解釈器を起動すると小さな整数プールが作成され、これらのオブジェクトが自動的に作成されてメモリにロードされて呼び出しを待つため、初期参照は様々であることがわかります.問題が発生しました.この参照カウント変数は、2つのスレッドが同時に値を増加または減少させる競合条件を回避するために保護する必要があります.このような場合、未解放の漏洩メモリが発生する可能性があります.さらに悪いことに、オブジェクトへの参照がまだ存在する場合、メモリが誤って解放されます.これはPythonプログラムのクラッシュを引き起こす可能性があります.この参照カウント変数は、スレッド間で共有されるすべてのデータ構造にロックを追加することによって安全を維持することができ、不一致に変更されない.ただし、各オブジェクトまたはオブジェクトグループにロックを追加すると、複数のロックが存在し、別の問題のデッドロックが発生する可能性があります(複数のロックが存在する場合にのみデッドロックが発生します).もう1つの副作用は、ロックの取得と解放を繰り返すと、パフォーマンスが低下することです.GILは、任意のPythonバイトコードを実行するには、解釈器ロックを取得する必要があるというルールを追加した解釈器自体のロックです.これにより、デッドロックが防止され(ロックが1つしかないため)、パフォーマンスオーバーヘッドが発生しません.
マルチスレッドプログラムへのGILの影響
cpu密集型プログラムを例にとると、
実行出力2.386808098030099033(cpuはi 7)
実行出力2.2348098754882812は、2つのバージョンの完了時間がほぼ同じです.マルチスレッドバージョンでは、GILはスレッドの並列実行をブロックします.ただし、GILはI/O密集型のマルチスレッドプログラムの性能にあまり影響しません.スレッドがI/Oを待っている間にスレッド間で共有されるためです.
どうしよう
1つの簡単な方法はマルチプロセスを使用することであり、各Pythonプロセスには独自のPython解釈器とメモリ空間があるため、GILは問題にならない.例は以下の通りである.
実行出力1.454556941986084は、実行速度が向上したが、マルチプロセスがマルチスレッドよりも重く、パフォーマンス消費も大きいため、予想通りに倍増していないことがわかります.
リファレンス
https://realpython.com/python-gil/
GILはGlobal Interpreter Lockの略で、Python解釈器を1つのスレッドで制御できる反発ロックです.つまり、同じ時間に1つのスレッドしか実行できないということです.複数のスレッドが同時に起動しても、並列実行ではありません.このGILは単一スレッドプログラムには影響がなく,マルチスレッドプログラムには影響が大きいが,CPU密集型プログラムではマルチスレッドを起動するか起動しないかで性能に差がないので,後でコードで検証する.
GILのPythonでの用途
Pythonは参照カウントを使用してメモリ管理を行うことが知られています.Pythonで作成されたオブジェクトには、オブジェクトへの参照の数を追跡する参照カウント変数があります.このカウントがゼロに達すると、オブジェクトが消費するメモリが解放されます.
>>> import sys
>>> a = []
>>> sys.getrefcount(a)
2
>>> sys.getrefcount([])
1
>>> b = a
>>> sys.getrefcount(a)
3
>>> sys.getrefcount(a)
3
上の例のように、aとsys.getrefcountは2回参照しているので、出力参照回数は2で、直接sysであれば.getrefcount([])はsysのみです.getrefcountの1回の参照は、b=aであれば、もう一度参照を計算するのでsys.getrefcount(a)出力は3回の参照であるが、再びsys.getrefcount(a)はなぜ3なのか.そうじゃないgetrefcountも1回の参照ですか?これは、aが変数としてsysに伝達するためである.getrefcountでは確かに1回の参照を計算しますが、この関数の実行が完了すると、この参照も減ってしまうので、この関数を何度も実行しても同じように見えます.a=-5~256の場合、Pythonが解釈器を起動すると小さな整数プールが作成され、これらのオブジェクトが自動的に作成されてメモリにロードされて呼び出しを待つため、初期参照は様々であることがわかります.問題が発生しました.この参照カウント変数は、2つのスレッドが同時に値を増加または減少させる競合条件を回避するために保護する必要があります.このような場合、未解放の漏洩メモリが発生する可能性があります.さらに悪いことに、オブジェクトへの参照がまだ存在する場合、メモリが誤って解放されます.これはPythonプログラムのクラッシュを引き起こす可能性があります.この参照カウント変数は、スレッド間で共有されるすべてのデータ構造にロックを追加することによって安全を維持することができ、不一致に変更されない.ただし、各オブジェクトまたはオブジェクトグループにロックを追加すると、複数のロックが存在し、別の問題のデッドロックが発生する可能性があります(複数のロックが存在する場合にのみデッドロックが発生します).もう1つの副作用は、ロックの取得と解放を繰り返すと、パフォーマンスが低下することです.GILは、任意のPythonバイトコードを実行するには、解釈器ロックを取得する必要があるというルールを追加した解釈器自体のロックです.これにより、デッドロックが防止され(ロックが1つしかないため)、パフォーマンスオーバーヘッドが発生しません.
マルチスレッドプログラムへのGILの影響
cpu密集型プログラムを例にとると、
#
import time
COUNT = 50000000
def countdown(n):
while n>0:
n -= 1
start = time.time()
countdown(COUNT)
end = time.time()
print(end - start)
実行出力2.386808098030099033(cpuはi 7)
#
import time
from threading import Thread
COUNT = 50000000
def countdown(n):
while n>0:
n -= 1
t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))
start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print(end - start)
実行出力2.2348098754882812は、2つのバージョンの完了時間がほぼ同じです.マルチスレッドバージョンでは、GILはスレッドの並列実行をブロックします.ただし、GILはI/O密集型のマルチスレッドプログラムの性能にあまり影響しません.スレッドがI/Oを待っている間にスレッド間で共有されるためです.
どうしよう
1つの簡単な方法はマルチプロセスを使用することであり、各Pythonプロセスには独自のPython解釈器とメモリ空間があるため、GILは問題にならない.例は以下の通りである.
from multiprocessing import Pool
import time
COUNT = 50000000
def countdown(n):
while n>0:
n -= 1
if __name__ == '__main__':
pool = Pool(processes=2)
start = time.time()
r1 = pool.apply_async(countdown, [COUNT//2])
r2 = pool.apply_async(countdown, [COUNT//2])
pool.close()
pool.join()
end = time.time()
print(end - start)
実行出力1.454556941986084は、実行速度が向上したが、マルチプロセスがマルチスレッドよりも重く、パフォーマンス消費も大きいため、予想通りに倍増していないことがわかります.
リファレンス
https://realpython.com/python-gil/