Der Inhalt dieses Artikels befasst sich mit der Frage, was Pythons GIL ist. Die Einführung von GIL in Python hat einen gewissen Referenzwert. Freunde in Not können sich darauf beziehen.
GIL (Global Interpreter Lock) ist keine Funktion von Python, sondern ein Konzept, das vom Python-Interpreter Cpython eingeführt wurde. Der Interpreter von Python ist nicht nur Cpython. Wenn der Interpreter Jpython ist, verfügt Python nicht über GIL.
Werfen wir einen Blick auf die offizielle Erklärung:
In CPython ist die globale Interpretersperre (GIL) ein Mutex, der verhindert, dass mehrere native Threads Python-Bytecodes ausführen Diese Sperre ist hauptsächlich deshalb notwendig, weil CPythons Speicherverwaltung nicht threadsicher ist. (Seitdem die GIL existiert, sind andere Funktionen jedoch zunehmend von den Garantien abhängig, die sie erzwingt.)
Ein Mutex (Mutex-Sperre), der verhindert, dass mehrere Threads gleichzeitig Maschinencode ausführen. Der Grund ist: Die Speicherverwaltung von Cpython ist nicht threadsicher.
Weil aus physischen Gründen Einschränkungen wurde der Wettbewerb zwischen CPU-Herstellern bei der Kernfrequenz durch Multi-Core ersetzt. Um die Leistung von Multi-Core-Prozessoren effektiver zu nutzen, ist die Multi-Thread-Programmierung entstanden, und damit verbunden ist die Schwierigkeit der Datenkonsistenz und Statussynchronisierung zwischen Threads. Auch der Cache innerhalb der CPU stellt hier keine Ausnahme dar. Um das Problem der Datensynchronisation zwischen mehreren Caches effektiv zu lösen, haben verschiedene Hersteller große Anstrengungen unternommen, was unweigerlich zu einem gewissen Leistungsverlust führt.
Natürlich kann Python nicht entkommen. Um die Vorteile mehrerer Kerne zu nutzen, begann Python, Multithreading zu unterstützen. Der einfachste Weg, die Datenintegrität und Statussynchronisierung zwischen Multithreads zu lösen, ist natürlich das Sperren. Es gibt also die super große Sperre von GIL, und wenn immer mehr Codebasisentwickler diese Einstellung akzeptieren, beginnen sie, sich stark auf diese Funktion zu verlassen (das heißt, die standardmäßigen internen Python-Objekte sind threadsicher, und es besteht keine Notwendigkeit dafür). Die Implementierung berücksichtigt zusätzliche Speichersperren und Synchronisierungsvorgänge.
Langsam stellte sich heraus, dass diese Implementierungsmethode schmerzhaft und ineffizient war. Aber als alle versuchten, die GIL aufzuspalten und zu entfernen, stellten sie fest, dass sich eine große Anzahl von Bibliothekscodeentwicklern stark auf die GIL verlassen hatte und es sehr schwierig war, sie zu entfernen. Wie schwierig ist es? Um eine Analogie zu geben: Ein „kleines Projekt“ wie MySQL hat fast fünf Jahre in mehreren Hauptversionen von 5.5 über 5.6 bis 5.7 verbracht, um die große Sperre von Buffer Pool Mutex in verschiedene kleine Sperren aufzuteilen, und es wird immer noch fortgesetzt. MySQL, ein Produkt mit Unternehmensunterstützung und einem festen Entwicklungsteam dahinter, hat es so schwer, ganz zu schweigen von einem stark gemeinschaftsbasierten Team aus Kernentwicklern und Code-Mitwirkenden wie Python?
Einfach ausgedrückt hat die Existenz von GIL eher historische Gründe. Wenn wir alles noch einmal machen müssten, stünden wir immer noch vor dem Problem des Multithreadings, aber zumindest wäre es eleganter als der aktuelle GIL-Ansatz.
Aus der obigen Einleitung und der offiziellen Definition geht hervor, dass GIL zweifellos ein weltweit exklusives Schloss ist. Es besteht kein Zweifel, dass die Existenz globaler Sperren einen großen Einfluss auf die Effizienz von Multithreading haben wird. Es ist fast so, als ob Python ein Single-Thread-Programm wäre. Dann werden die Leser sagen, dass die Effizienz nicht schlecht sein wird, solange die globale Sperre aufgehoben wird. Solange die GIL bei zeitaufwändigen E/A-Vorgängen freigegeben werden kann, kann die Betriebseffizienz noch verbessert werden. Mit anderen Worten, egal wie schlimm es ist, es wird nicht schlechter sein als die Effizienz eines einzelnen Threads. Das stimmt in der Theorie, aber in der Praxis? Python ist schlimmer als Sie denken.
Vergleichen wir die Effizienz von Python beim Multi-Threading und Single-Threading. Die Testmethode ist sehr einfach, eine Zählerfunktion, die 100 Millionen Mal wiederholt wird. Einer wird zweimal durch einen einzelnen Thread ausgeführt, und einer wird durch mehrere Threads ausgeführt. Vergleichen Sie abschließend die Gesamtausführungszeit. Die Testumgebung ist ein Dual-Core Mac Pro. Hinweis: Um die Auswirkungen des Leistungsverlusts der Thread-Bibliothek selbst auf die Testergebnisse zu verringern, verwendet der Single-Thread-Code hier auch Threads. Führen Sie es einfach zweimal nacheinander aus, um einen einzelnen Thread zu simulieren.
#! /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()
Das kann Es ist ersichtlich, dass Python im Multithread-Fall tatsächlich 45 % langsamer ist als ein einzelner Thread. Laut der vorherigen Analyse sollte serialisiertes Multithreading selbst bei Vorhandensein einer globalen GIL-Sperre die gleiche Effizienz wie Singlethreading haben. Wie konnte es also zu einem so schlechten Ergebnis kommen?
Lassen Sie uns die Gründe dafür anhand des Implementierungsprinzips von GIL analysieren.
Laut der Python-Community ist die Thread-Planung des Betriebssystems selbst bereits sehr ausgereift und stabil, und es ist nicht nötig, es selbst zu machen. Machen Sie ein Set. Daher ist ein Python-Thread ein P-Thread in der C-Sprache und wird über den Planungsalgorithmus des Betriebssystems geplant (z. B. ist Linux CFS). Damit jeder Thread die CPU-Zeit gleichmäßig nutzen kann, berechnet Python die Anzahl der aktuell ausgeführten Mikrocodes und erzwingt die Freigabe der GIL, wenn sie einen bestimmten Schwellenwert erreicht. Zu diesem Zeitpunkt wird auch die Thread-Planung des Betriebssystems ausgelöst (ob der Kontextwechsel tatsächlich durchgeführt wird, wird natürlich vom Betriebssystem bestimmt).
Pseudocode
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在较长一段时间内将会继续存在,但是会不断对其进行改进
Das obige ist der detaillierte Inhalt vonWas ist Pythons GIL? Einführung in GIL in Python. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!