Heim > Artikel > Backend-Entwicklung > Was ist die GIL in Python?
GIL ist im Wesentlichen eine Sperre. Studenten, die Betriebssysteme studiert haben, wissen, dass Sperren eingeführt werden, um Dateninkonsistenzen zu vermeiden, die durch gleichzeitigen Zugriff verursacht werden. Es gibt viele globale Variablen, die außerhalb von Funktionen in CPython definiert sind, wie z. B. usable_arenas und usedpools in der Speicherverwaltung. Wenn mehrere Threads gleichzeitig Speicher beantragen, können diese Variablen gleichzeitig geändert werden, was zu Datenverwirrung führt. Darüber hinaus basiert der Garbage-Collection-Mechanismus von Python auf der Referenzzählung. Alle Objekte verfügen über ein ob_refcnt-Feld, das angibt, wie viele Variablen derzeit auf das aktuelle Objekt verweisen Die Funktion reduziert die Anzahl der Referenzen. Wenn mehrere Threads gleichzeitig den Referenzzähler desselben Objekts ändern, ist es möglich, dass ob_refcnt vom tatsächlichen Wert abweicht, was zu Speicherverlusten führen kann. Objekte, die nicht verwendet werden, werden nicht recycelt und vieles mehr Im Ernst, sie könnten recycelt werden. Das referenzierte Objekt verursachte einen Absturz des Python-Interpreters.
Die Definition von GIL in CPython lautet wie folgt:
struct _gil_runtime_state { unsigned long interval; // 请求 GIL 的线程在 interval 毫秒后还没成功,就会向持有 GIL 的线程发出释放信号 _Py_atomic_address last_holder; // GIL 上一次的持有线程,强制切换线程时会用到 _Py_atomic_int locked; // GIL 是否被某个线程持有 unsigned long switch_number; // GIL 的持有线程切换了多少次 // 条件变量和互斥锁,一般都是成对出现 PyCOND_T cond; PyMUTEX_T mutex; // 条件变量,用于强制切换线程 PyCOND_T switch_cond; PyMUTEX_T switch_mutex; };
Das Wichtigste ist das durch Mutex geschützte gesperrte Feld, das angibt, ob GIL derzeit vorhanden ist. Andere Felder werden zur Optimierung von GIL verwendet. Wenn ein Thread GIL beantragt, ruft er die Methode take_gil() auf, und wenn er GIL freigibt, ruft er die Methode drop_gil() auf. Um eine Hungersnot zu vermeiden, sendet ein Thread, wenn er auf Intervall-Millisekunden wartet (Standard ist 5 Millisekunden) und sich nicht für GIL beworben hat, aktiv ein Signal an den Thread, der GIL hält, und der GIL-Inhaber prüft das Signal zu gegebener Zeit . Wenn festgestellt wird, dass andere Threads gelten, wird die GIL zwangsweise freigegeben. Das hier erwähnte geeignete Timing ist in den verschiedenen Versionen unterschiedlich. In den frühen Tagen wurde es alle 100 Anweisungen überprüft. In Python 3.10.4 wurde es am Ende der bedingten Anweisung überprüft, am Ende jedes Schleifenkörpers der Schleifenanweisung , und das Ende des Funktionsaufrufs wird zu gegebener Zeit überprüft.
Die Funktion take_gil(), die für GIL gilt, wird wie folgt vereinfacht:
static void take_gil(PyThreadState *tstate) { ... // 申请互斥锁 MUTEX_LOCK(gil->mutex); // 如果 GIL 空闲就直接获取 if (!_Py_atomic_load_relaxed(&gil->locked)) { goto _ready; } // 尝试等待 while (_Py_atomic_load_relaxed(&gil->locked)) { unsigned long saved_switchnum = gil->switch_number; unsigned long interval = (gil->interval >= 1 ? gil->interval : 1); int timed_out = 0; COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out); if (timed_out && _Py_atomic_load_relaxed(&gil->locked) && gil->switch_number == saved_switchnum) { SET_GIL_DROP_REQUEST(interp); } } _ready: MUTEX_LOCK(gil->switch_mutex); _Py_atomic_store_relaxed(&gil->locked, 1); _Py_ANNOTATE_RWLOCK_ACQUIRED(&gil->locked, /*is_write=*/1); if (tstate != (PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) { _Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate); ++gil->switch_number; } // 唤醒强制切换的线程主动等待的条件变量 COND_SIGNAL(gil->switch_cond); MUTEX_UNLOCK(gil->switch_mutex); if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request)) { RESET_GIL_DROP_REQUEST(interp); } else { COMPUTE_EVAL_BREAKER(interp, ceval, ceval2); } ... // 释放互斥锁 MUTEX_UNLOCK(gil->mutex); }
Um die Atomizität sicherzustellen, muss der gesamte Funktionskörper am Anfang bzw. am Ende die Mutex-Sperre gil->mutex beantragen und freigeben. Wenn die aktuelle GIL inaktiv ist, rufen Sie die GIL direkt ab. Wenn sie nicht inaktiv ist, warten Sie auf das Millisekunden-Intervall der Bedingungsvariablen (nicht weniger als 1 Millisekunde), wenn während des Zeitraums keine GIL-Umschaltung erfolgt , setzen Sie gil_drop_request, um einen erzwungenen Wechsel anzufordern. Die GIL hält den Thread, andernfalls wartet sie weiter. Sobald die GIL erfolgreich abgerufen wurde, müssen die Werte von gil->locked, gil->last_holder und gil->switch_number aktualisiert werden, die Bedingungsvariable gil->switch_cond muss aktiviert werden und die Mutex-Sperre muss aktiviert werden gil->mutex muss freigegeben werden.
Die Funktion drop_gil(), die GIL freigibt, wird wie folgt vereinfacht:
static void drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2, PyThreadState *tstate) { ... if (tstate != NULL) { _Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate); } MUTEX_LOCK(gil->mutex); _Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1); // 释放 GIL _Py_atomic_store_relaxed(&gil->locked, 0); // 唤醒正在等待 GIL 的线程 COND_SIGNAL(gil->cond); MUTEX_UNLOCK(gil->mutex); if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request) && tstate != NULL) { MUTEX_LOCK(gil->switch_mutex); // 强制等待一次线程切换才被唤醒,避免饥饿 if (((PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) == tstate) { assert(is_tstate_valid(tstate)); RESET_GIL_DROP_REQUEST(tstate->interp); COND_WAIT(gil->switch_cond, gil->switch_mutex); } MUTEX_UNLOCK(gil->switch_mutex); } }
Gilt zuerst die GIL unter dem Schutz von gil->mutex und weckt dann andere Threads auf, die auf die GIL warten. In einer Umgebung mit mehreren CPUs besteht eine höhere Wahrscheinlichkeit, dass der aktuelle Thread die GIL nach der Freigabe der GIL erneut erhält. Um ein Aushungern anderer Threads zu vermeiden, muss der aktuelle Thread gezwungen werden, auf die Bedingungsvariable gil->switch_cond zu warten . Es kann die GIL nur erhalten, wenn andere Threads nur dann aktiviert werden.
Durch GIL eingeschränkter Code kann nicht parallel ausgeführt werden, was die Gesamtleistung verringert. Um Leistungsverluste zu minimieren, führt Python aktiv E/A-Vorgänge oder intensive CPU-Berechnungen aus, die keinen Objektzugriff beinhalten. Durch die Freigabe der GIL wird die Granularität der GIL reduziert, z. B.
Dateien lesen und schreiben
Netzwerkzugriff
verschlüsselte Daten/komprimierte Daten
Genau genommen also im Fall eines einzelnen Prozesses , mehr Zwei Python-Threads können gleichzeitig ausgeführt werden. Beispielsweise läuft ein Thread normal und ein anderer Thread komprimiert Daten.
GIL ist eine Sperre, die generiert wird, um die Konsistenz interner Variablen des Python-Interpreters aufrechtzuerhalten. Die Konsistenz von Benutzerdaten ist nicht für GIL verantwortlich. Obwohl GIL bis zu einem gewissen Grad auch die Konsistenz von Benutzerdaten gewährleistet, werden Anweisungen, die in Python 3.10.4 keine Sprünge und Funktionsaufrufe beinhalten, atomar unter den Einschränkungen von GIL ausgeführt, aber die Konsistenz von Daten in der Geschäftslogik Der Benutzer muss es selbst sperren, um dies sicherzustellen.
Der folgende Code verwendet zwei Threads, um die Sammlung von Fragmenten des Benutzers zu simulieren, um Auszeichnungen zu gewinnen.
from threading import Thread def main(): stat = {"piece_count": 0, "reward_count": 0} t1 = Thread(target=process_piece, args=(stat,)) t2 = Thread(target=process_piece, args=(stat,)) t1.start() t2.start() t1.join() t2.join() print(stat) def process_piece(stat): for i in range(10000000): if stat["piece_count"] % 10 == 0: reward = True else: reward = False if reward: stat["reward_count"] += 1 stat["piece_count"] += 1 if __name__ == "__main__": main()
Angenommen, der Benutzer kann jedes Mal eine Belohnung erhalten, wenn er 10 Fragmente sammelt, und sollte 9999999 Belohnungen erhalten (beim letzten Mal). es wurde keine Belohnung berechnet), insgesamt sollten 20.000.000 Fragmente gesammelt werden und 1.999.998 Belohnungen sollten gesammelt werden. Die Ergebnisse des ersten Laufs auf meinem Computer sind jedoch wie folgt:
{'piece_count': 20000000, 'reward_count': 1999987}
Die Gesamtzahl der Fragmente ist wie erwartet, aber die Anzahl der Belohnungen ist 12 weniger. Die Anzahl der Teile ist korrekt, da in Python 3.10.4 stat["piece_count"] += 1 unter GIL-Einschränkungen atomar ausgeführt wird. Da der Ausführungsthread am Ende jeder Schleife umgeschaltet werden kann, ist es möglich, dass Thread t1 am Ende einer bestimmten Schleife die Stückzahl auf 100 erhöht, aber bevor die nächste Schleife mit der Beurteilung von Modulo 10 beginnt, wechselt der Python-Interpreter zum Thread t2 für die Ausführung, und t2 erhöht die Stückzahl. Wenn Sie 101 erreichen, verpassen Sie eine Belohnung.
Anhang: So vermeiden Sie, von GIL betroffen zu sein
Nachdem ich so viel gesagt habe: Wenn ich die Lösung nicht erwähne, ist es nur ein populärwissenschaftlicher Beitrag, aber er ist nutzlos. GIL ist so schlimm, gibt es einen Ausweg? Werfen wir einen Blick auf die verfügbaren Lösungen.
Thread durch Multiprozess ersetzen
Das Aufkommen der Multiprozessbibliothek dient größtenteils dazu, die Ineffizienz der Threadbibliothek aufgrund von GIL auszugleichen. Es repliziert vollständig eine Reihe von Schnittstellen, die vom Thread bereitgestellt werden, um die Migration zu erleichtern. Der einzige Unterschied besteht darin, dass mehrere Prozesse anstelle mehrerer Threads verwendet werden. Jeder Prozess verfügt über seine eigene unabhängige GIL, sodass es keinen GIL-Konflikt zwischen Prozessen gibt.
Natürlich ist Multiprozess kein Allheilmittel. Seine Einführung wird die Schwierigkeit der Datenkommunikation und Synchronisierung zwischen Zeitthreads im Programm erhöhen. Nehmen wir den Zähler als Beispiel. Wenn wir möchten, dass mehrere Threads dieselbe Variable akkumulieren, deklarieren Sie für Thread eine globale Variable und umschließen Sie drei Zeilen mit dem Thread.Lock-Kontext. Da die Prozesse in Multiprozessen die Daten der anderen nicht sehen können, können sie nur eine Warteschlange im Hauptthread deklarieren, ablegen und dann abrufen oder gemeinsam genutzten Speicher verwenden. Diese zusätzlichen Implementierungskosten machen das Programmieren von Multithread-Programmen, das ohnehin schon sehr mühsam ist, noch mühsamer. Wo liegen die spezifischen Schwierigkeiten? Interessierte Leser können diesen Artikel näher erläutern
Andere Parser verwenden
Sind, wie bereits erwähnt, andere Parser besser, da GIL nur ein Produkt von CPython ist? Ja, Parser wie JPython und IronPython benötigen aufgrund der Art ihrer Implementierungssprachen nicht die Hilfe der GIL. Durch die Verwendung von Java/C# für die Parser-Implementierung verloren sie jedoch auch die Möglichkeit, die vielen nützlichen Funktionen der C-Sprachmodule der Community zu nutzen. Daher waren diese Parser schon immer eine relativ Nische. Schließlich wird jeder im Anfangsstadium ersteres über Funktion und Leistung entscheiden. Fertig ist besser als perfekt.
Also ist es hoffnungslos?
Natürlich arbeitet die Python-Community auch sehr hart daran, die GIL kontinuierlich zu verbessern und versucht sogar, die GIL zu entfernen. Und in jeder Nebenversion gab es viele Verbesserungen. Interessierte Leser können diese Folie weiterlesen
Eine weitere Verbesserung: Überarbeitung der GIL
– Ändern Sie die Umschaltgranularität von Opcode-basierter Zählung zu zeitscheibenbasierter Zählung
– Vermeiden Sie, dass der Thread, der kürzlich die GIL-Sperre aufgehoben hat, sofort blockiert wird erneute Planung
– Thread-Prioritätsfunktion hinzugefügt (Threads mit hoher Priorität können andere Threads zwingen, die von ihnen gehaltenen GIL-Sperren aufzuheben)
Das obige ist der detaillierte Inhalt vonWas ist die GIL in Python?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!