今回の記事の内容はPythonのGILとは何か? Python での GIL の導入は一定の参考価値があるので、困っている友人は参考にしていただければ幸いです。
Cpython によって導入された概念です。 Python のインタプリタは Cpython だけではありません、インタプリタが Jpython の場合、Python には GIL がありません。
公式の説明を見てみましょう: CPython では、グローバル インタプリタ ロック (GIL) は、複数のネイティブ スレッドが Python バイトコードを実行するのを防ぐmutex です。このロックが必要なのは、主に CPython のメモリ管理がスレッドセーフではないためです。 (ただし、GIL が存在するため、他の機能は GIL が強制する保証に依存するようになりました。)
複数のスレッドがマシン コードを同時に実行することを防ぐミューテックス (ミューテックス ロック) その理由は次のとおりです: Cpython のメモリ管理はスレッドセーフではありませんなぜ GIL があるのか理由は物理的な理由によるものです。制限があるため、コア周波数における CPU メーカー間の競争はマルチコアに取って代わられました。マルチコアプロセッサの性能をより有効に活用するために、マルチスレッドプログラミングが登場しましたが、それに伴いスレッド間のデータの整合性やステータスの同期が困難になってきました。 CPU 内のキャッシュも例外ではなく、複数のキャッシュ間のデータ同期の問題を効果的に解決するために、さまざまなメーカーが多大な労力を費やしており、必然的に一定のパフォーマンスの低下が生じます。 もちろん Python も逃げることはできず、マルチコアの利点を活かすために、Python はマルチスレッドをサポートし始めました。複数のスレッド間のデータの整合性とステータスの同期 を解決する最も簡単な方法は、当然ロックすることです。そのため、GIL には非常に大きなロックがあり、この設定を受け入れるコード ベース開発者が増えると、彼らはこの機能に大きく依存し始めます (つまり、デフォルトの Python 内部オブジェクトはスレッドセーフであり、スレッド セーフである必要はありません)。実装では、追加のメモリ ロックと同期操作が考慮されます)。
徐々に、この実装方法は面倒で非効率であることが判明しました。しかし、全員が GIL を分割して削除しようとしたところ、多くのライブラリ コード開発者が GIL に大きく依存しており、GIL を削除するのが非常に難しいことがわかりました。どれくらい難しいですか?例えるなら、MySQL のような「小さなプロジェクト」は、Buffer Pool Mutex の大きなロックを 5.5、5.6、5.7 のさまざまな小さなロックに分割するのに 5 年近くかかりましたが、それはまだ継続中です。 MySQL は企業のサポートとその背後に固定の開発チームがいる製品ですが、コア開発者と Python のようなコード貢献者からなる高度にコミュニティベースのチームは言うまでもなく、これほど困難な状況に陥っているのでしょうか? つまり、簡単に言えば、GIL の存在は歴史的な理由によるものです。これを最初からやり直す必要がある場合でも、マルチスレッドの問題に直面することになりますが、少なくとも現在の GIL アプローチよりは洗練されるでしょう。 GIL の影響上記の紹介と公式の定義から判断すると、GIL は間違いなくグローバル排他ロックです。グローバル ロックの存在がマルチスレッドの効率に大きな影響を与えることは疑いの余地がありません。あたかも Python がシングルスレッド プログラムであるかのようです。そうなると読者は、グローバルロックが解除されていれば効率は悪くないと言うでしょう。時間のかかる IO 操作を実行するときに GIL を解放できる限り、操作効率を向上させることができます。つまり、どんなに悪くてもシングルスレッドの効率より悪くなることはありません。これは理論的には正しいですが、実際にはどうなのでしょうか? Python はあなたが思っているよりも悪いです。 マルチスレッドとシングルスレッドでの Python の効率を比較してみましょう。テスト方法は非常にシンプルで、1億回ループするカウンター関数です。 1 つは単一のスレッドで 2 回実行され、もう 1 つは複数のスレッドで実行されます。最後に合計実行時間を比較します。テスト環境はデュアルコアMac proです。注: スレッド ライブラリ自体のパフォーマンス損失がテスト結果に及ぼす影響を軽減するために、ここでのシングルスレッド コードではスレッドも使用されます。単一のスレッドをシミュレートするには、これを 2 回連続して実行するだけです。 単一スレッドを順次実行 (single_thread.py)#! /usr/bin/python from threading import Thread import time def my_counter(): i = 0 for _ in range(100000000): i = i + 1 return True def main(): thread_array = {} start_time = time.time() for tid in range(2): t = Thread(target=my_counter) t.start() t.join() end_time = time.time() print("Total time: {}".format(end_time - start_time)) if __name__ == '__main__': main()
#! /usr/bin/python from threading import Thread import time def my_counter(): i = 0 for _ in range(100000000): i = i + 1 return True def main(): thread_array = {} start_time = time.time() for tid in range(2): t = Thread(target=my_counter) t.start() thread_array[tid] = t for i in range(2): thread_array[i].join() end_time = time.time() print("Total time: {}".format(end_time - start_time)) if __name__ == '__main__': main()
# #できるマルチスレッドの場合、Python はシングルスレッドよりも実際に 45% 遅いことがわかります。前の分析によると、GIL グローバル ロックが存在する場合でも、シリアル化されたマルチスレッドはシングル スレッドと同じ効率を持つはずです。では、どうしてこのような悪い結果が生じるのでしょうか?
GIL の実装原則を通じて、この理由を分析してみましょう。
現在の GIL 設計の欠陥
疑似コード
while True: acquire GIL for i in 1000: do something release GIL /* Give Operating System a chance to do thread scheduling */
这种模式在只有一个CPU核心的情况下毫无问题。任何一个线程被唤起时都能成功获得到GIL(因为只有释放了GIL才会引发线程调度)。但当CPU有多个核心的时候,问题就来了。从伪代码可以看到,从release GIL到acquire GIL之间几乎是没有间隙的。所以当其他在其他核心上的线程被唤醒时,大部分情况下主线程已经又再一次获取到GIL了。这个时候被唤醒执行的线程只能白白的浪费CPU时间,看着另一个线程拿着GIL欢快的执行着。然后达到切换时间后进入待调度状态,再被唤醒,再等待,以此往复恶性循环。
PS:当然这种实现方式是原始而丑陋的,Python的每个版本中也在逐渐改进GIL和线程调度之间的互动关系。例如先尝试持有GIL在做线程上下文切换,在IO等待时释放GIL等尝试。但是无法改变的是GIL的存在使得操作系统线程调度的这个本来就昂贵的操作变得更奢侈了。
为了直观的理解GIL对于多线程带来的性能影响,这里直接借用的一张测试结果图(见下图)。图中表示的是两个线程在双核CPU上得执行情况。两个线程均为CPU密集型运算线程。绿色部分表示该线程在运行,且在执行有用的计算,红色部分为线程被调度唤醒,但是无法获取GIL导致无法进行有效运算等待的时间。
由图可见,GIL的存在导致多线程无法很好的立即多核CPU的并发处理能力。
那么Python的IO密集型线程能否从多线程中受益呢?我们来看下面这张测试结果。颜色代表的含义和上图一致。白色部分表示IO线程处于等待。可见,当IO线程收到数据包引起终端切换后,仍然由于一个CPU密集型线程的存在,导致无法获取GIL锁,从而进行无尽的循环等待。
简单的总结下就是:Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。
multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。
当然multiprocessing也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。
之前也提到了既然GIL只是CPython的产物,那么其他解析器是不是更好呢?没错,像JPython和IronPython这样的解析器由于实现语言的特性,他们不需要GIL的帮助。然而由于用了Java/C#用于解析器实现,他们也失去了利用社区众多C语言模块有用特性的机会。所以这些解析器也因此一直都比较小众。毕竟功能和性能大家在初期都会选择前者,Done is better than perfect。
Python GIL其实是功能和性能之间权衡后的产物,它尤其存在的合理性,也有较难改变的客观因素。从本分的分析中,我们可以做以下一些简单的总结:
因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能
如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或者索性用其他语言实现
GIL在较长一段时间内将会继续存在,但是会不断对其进行改进
以上がPythonのGILとは何ですか? Python での GIL の概要の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。