Python hat in letzter Zeit viel Aufmerksamkeit erhalten. Mit der Veröffentlichung von 3.13, die für Oktober dieses Jahres geplant ist, beginnt die gewaltige Arbeit zur Entfernung der GIL. Für neugierige Benutzer, die ein (fast) GIL-freies Python ausprobieren möchten, ist bereits eine Vorabversion verfügbar.
Dieser ganze Hype hat mich dazu gebracht, mich in meiner eigenen Sprache, ArkScript, zu vertiefen, da ich in der Vergangenheit auch eine globale VM-Sperre hatte (hinzugefügt in Version 3.0.12 im Jahr 2020, entfernt in 3.1.3 im Jahr 2022). Vergleichen Sie die Dinge und zwingen Sie mich, tiefer in das Wie und Warum der Python-GIL einzutauchen.
Eine globale Interpretersperre (GIL) ist ein Mechanismus, der in Computerspracheninterpretern verwendet wird, um die Ausführung von Threads zu synchronisieren, sodass nur ein nativer Thread (pro Prozess) grundlegende Operationen (wie Speicherzuweisung und Referenzzählung) gleichzeitig ausführen kann Zeit.
Wikipedia – Globale Interpretersperre
Parallelität liegt vor, wenn zwei oder mehr Aufgaben in überlappenden Zeiträumen starten, ausgeführt und abgeschlossen werden können, das bedeutet aber nicht, dass sie beide gleichzeitig ausgeführt werden.
Parallelität bedeutet, dass Aufgaben buchstäblich gleichzeitig ausgeführt werden, z. B. auf einem Multicore-Prozessor.
Eine ausführliche Erklärung finden Sie in dieser Stack Overflow-Antwort.
Die GIL kann die Geschwindigkeit von Single-Thread-Programmen erhöhen, da Sie nicht alle Datenstrukturen sperren und freigeben müssen: Der gesamte Interpreter ist gesperrt, sodass Sie standardmäßig sicher sind.
Da es jedoch eine GIL pro Interpreter gibt, schränkt dies die Parallelität ein: Sie müssen einen völlig neuen Interpreter in einem separaten Prozess erzeugen (unter Verwendung des Multiprocessing-Moduls anstelle von Threading), um mehr als einen Kern zu verwenden! Dies ist mit höheren Kosten verbunden als nur das Erzeugen eines neuen Threads, da Sie sich jetzt um die Kommunikation zwischen Prozessen kümmern müssen, was einen nicht zu vernachlässigenden Overhead mit sich bringt (Benchmarks finden Sie unter „GeekPython – GIL Become Optional in Python 3.13“).
Im Fall von Python liegt es daran, dass die Hauptimplementierung, CPython, über keine threadsichere Speicherverwaltung verfügt. Ohne die GIL würde das folgende Szenario eine Racebedingung generieren:
Wenn Thread 1 zuerst ausgeführt wird, beträgt die Anzahl 11 (Anzahl * 2 = 10, dann Anzahl 1 = 11).
Wenn Thread 2 zuerst ausgeführt wird, beträgt die Anzahl 12 (Anzahl 1 = 6, dann Anzahl * 2 = 12).
Die Reihenfolge der Ausführung ist wichtig, aber es kann noch schlimmer passieren: Wenn beide Threads gleichzeitig count lesen, löscht einer das Ergebnis des anderen und count ist entweder 10 oder 6!
Insgesamt macht eine GIL die (CPython-)Implementierung im Allgemeinen einfacher und schneller:
Es erleichtert auch das Umschließen von C-Bibliotheken, da Ihnen dank der GIL Thread-Sicherheit garantiert ist.
Der Nachteil ist, dass Ihr Code asynchron ist, wie in gleichzeitig, aber nicht parallel.
[!NOTIZ]
Python 3.13 entfernt die GIL!Der PEP 703 hat eine Gebäudekonfiguration hinzugefügt –disable-gil, sodass Sie bei der Installation von Python 3.13 von Leistungsverbesserungen in Multithread-Programmen profitieren können.
In Python müssen Funktionen eine Farbe annehmen: Sie sind entweder „normal“ oder „asynchron“. Was bedeutet das in der Praxis?
>>> def foo(call_me): ... print(call_me()) ... >>> async def a_bar(): ... return 5 ... >>> def bar(): ... return 6 ... >>> foo(a_bar):2: RuntimeWarning: coroutine 'a_bar' was never awaited RuntimeWarning: Enable tracemalloc to get the object allocation traceback >>> foo(bar) 6
Da eine asynchrone Funktion nicht sofort einen Wert zurückgibt, sondern eine Coroutine aufruft, können wir sie nicht überall als Rückrufe verwenden, es sei denn, die von uns aufgerufene Funktion ist für die Annahme asynchroner Rückrufe ausgelegt.
Wir erhalten eine Hierarchie von Funktionen, da „normale“ Funktionen asynchron gemacht werden müssen, um das Schlüsselwort „await“ zu verwenden, das zum Aufrufen asynchroner Funktionen erforderlich ist:
can call normal -----------> normal can call async - -----------> normal | .-----------> async
Abgesehen davon, dass man dem Aufrufer vertraut, gibt es keine Möglichkeit zu wissen, ob ein Rückruf asynchron ist oder nicht (es sei denn, man versucht, ihn zuerst innerhalb eines Try/Except-Blocks aufzurufen, um nach einer Ausnahme zu suchen, aber das ist hässlich).
Am Anfang verwendete ArkScript eine globale VM-Sperre (ähnlich der GIL von Python), da das http.arkm-Modul (das zum Erstellen von HTTP-Servern verwendet wird) multithreaded war und Probleme mit der VM von ArkScript verursachte, indem es seinen Status durch Ändern von Variablen änderte und Aufrufen von Funktionen in mehreren Threads.
Im Jahr 2021 begann ich dann mit der Arbeit an einem neuen Modell zur Handhabung des VM-Status, damit wir ihn problemlos parallelisieren konnten, und schrieb einen Artikel darüber. Später wurde es bis Ende 2021 implementiert und die globale VM-Sperre wurde entfernt.
ArkScript weist asynchronen Funktionen keine Farbe zu, da sie in der Sprache nicht existieren: Sie haben entweder eine Funktion oder einen Abschluss, und beide können sich gegenseitig ohne zusätzliche Syntax aufrufen (ein Abschluss ist ein arme Man-Objekt, in dieser Sprache: eine Funktion, die einen veränderlichen Zustand hält).
Jede Funktion kann an der Aufrufseite asynchron gemacht werden (anstelle der Deklaration):
(let foo (fun (a b c) ( a b c))) (print (foo 1 2 3)) # 6 (let future (async foo 1 2 3)) (print future) # UserType (print (await future)) # 6 (print (await future)) # nil
Mithilfe der integrierten Funktion „async“ erzeugen wir unter der Haube ein std::future (unter Nutzung von std::async und Threads), um unsere Funktion mit einer Reihe von Argumenten auszuführen. Dann können wir „await“ (eine weitere integrierte Funktion) aufrufen und jederzeit ein Ergebnis erhalten, das den aktuellen VM-Thread blockiert, bis die Funktion zurückkehrt.
Somit ist es möglich, von jeder Funktion und von jedem Thread aus zu warten.
All dies ist möglich, weil wir eine einzelne VM haben, die einen Zustand bearbeitet, der in einem Ark::internal::ExecutionContext enthalten ist, der an einen einzelnen Thread gebunden ist. Die VM wird von den Threads gemeinsam genutzt, nicht von den Kontexten!
.---> thread 0, context 0 | ^ VM thread 1, context 1
Beim Erstellen einer Zukunft mithilfe von Async sind wir:
Dies verbietet jegliche Art der Synchronisierung zwischen Threads, da ArkScript keine Referenzen oder Sperren jeglicher Art offenlegt, die gemeinsam genutzt werden könnten (dies wurde aus Gründen der Einfachheit gemacht, da die Sprache etwas minimalistisch, aber dennoch verwendbar sein soll).
Dieser Ansatz ist jedoch nicht besser (und auch nicht schlechter) als der von Python, da wir pro Aufruf einen neuen Thread erstellen und die Anzahl der Threads pro CPU begrenzt ist, was etwas kostspielig ist. Glücklicherweise sehe ich darin kein Problem, das gelöst werden muss, da man niemals Hunderte oder Tausende von Threads gleichzeitig erstellen oder Hunderte oder Tausende von asynchronen Python-Funktionen gleichzeitig aufrufen sollte: Beides würde zu einer enormen Verlangsamung Ihres Programms führen.
Im ersten Fall würde dies Ihren Prozess (sogar Ihren Computer) verlangsamen, da das Betriebssystem damit beschäftigt ist, jedem Thread Zeit zu geben; im zweiten Fall müsste Pythons Scheduler zwischen all Ihren Coroutinen jonglieren.
[!NOTIZ]
Standardmäßig stellt ArkScript keine Mechanismen für die Thread-Synchronisierung bereit, aber selbst wenn wir einen UserType (der ein Wrapper über Typ-gelöschten C-Objekten ist) an eine Funktion übergeben, ist das zugrunde liegende Objekt nicht vorhanden. t kopiert.
Mit etwas sorgfältiger Codierung könnte man mithilfe des UserType-Konstrukts eine Sperre erstellen, die eine Synchronisierung zwischen Threads ermöglichen würde.(let lock (module:createLock)) (let foo (fun (lock i) { (lock true) (print (str:format "hello {}" i)) (lock false) })) (async foo lock 1) (async foo lock 2)
ArkScript und Python verwenden zwei sehr unterschiedliche Arten von Async/Await: Die erste erfordert die Verwendung von Async an der Aufrufstelle und erzeugt einen neuen Thread mit einem eigenen Kontext, während die zweite erfordert, dass der Programmierer Funktionen als asynchron markiert in der Lage sein, „await“ zu verwenden, und diese asynchronen Funktionen sind Coroutinen, die im selben Thread wie der Interpreter ausgeführt werden.
Ursprünglich von lexp.lt
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3