Heim  >  Artikel  >  Backend-Entwicklung  >  Vergleich der asynchronen Modelle Python und ArkScript

Vergleich der asynchronen Modelle Python und ArkScript

WBOY
WBOYOriginal
2024-09-04 06:51:54660Durchsuche

Comparing Python and ArkScript asynchronous models

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 mit meiner eigenen Sprache, ArkScript, zu beschäftigen, 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.

Definitionen

  1. Um zu beginnen, definieren wir, was eine GIL (Globale Interpretersperre) ist:

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

  1. Parallelität liegt vor, wenn zwei oder mehr Aufgaben in überlappenden Zeiträumen starten, ausgeführt und abgeschlossen werden können, das bedeutet jedoch nicht, dass sie beide gleichzeitig ausgeführt werden.

  2. Parallelität liegt vor, wenn 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.

Pythons GIL

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“).

Wie wirkt es sich auf die Asynchronität von Python aus?

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 Race-Bedingung erzeugen:

  1. Erstellen Sie eine gemeinsame Variablenanzahl = 5
  2. Thread 1: count *= 2
  3. Thread 2: Anzahl += 1

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 beträgt entweder 10 oder 6!

Insgesamt macht eine GIL die (CPython-)Implementierung im Allgemeinen einfacher und schneller:

  • Schneller im Single-Threaded-Fall (es ist nicht erforderlich, für jeden Vorgang eine Sperre zu erwerben/freizugeben)
  • Schneller im Multithread-Fall für IO-gebundene Programme (da diese außerhalb der GIL stattfinden)
  • Schneller im Multithread-Fall für CPU-gebundene Programme, die ihre rechenintensive Arbeit in C erledigen (da die GIL vor dem Aufruf des C-Codes freigegeben wird)

Außerdem wird das Umschließen von C-Bibliotheken einfacher, da Ihnen dank der GIL Thread-Sicherheit garantiert ist.

Der Nachteil ist, dass Ihr Code asynchron ist, wie in gleichzeitig, aber nicht parallel.

[!NOTE]
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.

Python-Async/Wait-Modell

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)
25c93a42aa09d6ad19dff8feb1023ce5
c2f32e7e9231c3bf5bf9f218b5147824: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 benötigt wird:

         can call
normal -----------> normal

         can call
async -+-----------> normal
       |
       .-----------> async                    

Abgesehen davon, dass man dem Aufrufer vertraut, gibt es keine Möglichkeit herauszufinden, 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).

ArkScript parallelism

In the beginning, ArkScript was using a Global VM Lock (akin to Python's GIL), because the http.arkm module (used to create HTTP servers) was multithreaded and it caused problems with ArkScript's VM by altering its state through modifying variables and calling functions on multiple threads.

Then in 2021, I started working on a new model to handle the VM state so that we could parallelize it easily, and wrote an article about it. It was later implemented by the end of 2021, and the Global VM Lock was removed.

ArkScript async/await

ArkScript does not assign a color to async functions, because they do not exist in the language: you either have a function or a closure, and both can call each other without any additional syntax (a closure is a poor man object, in this language: a function holding a mutable state).

Any function can be made async at the call site (instead of declaration):

(let foo (fun (a b c)
    (+ a b c)))

(print (foo 1 2 3))  # 6

(let future (async foo 1 2 3))
(print future)          # UserType8e288519637d83f9ffb95e89ea0f8cfb
(print (await future))  # 6
(print (await future))  # nil

Using the async builtin, we are spawning a std::future under the hood (leveraging std::async and threads) to run our function given a set of arguments. Then we can call await (another builtin) and get a result whenever we want, which will block the current VM thread until the function returns.

Thus, it is possible to await from any function, and from any thread.

The specificities

All of this is possible because we have a single VM that operates on a state contained inside an Ark::internal::ExecutionContext, which is tied to a single thread. The VM is shared between the threads, not the contexts!

        .---> thread 0, context 0
        |            ^
VM 1d166129c620ba7bd4551066df2b2cf5 thread 1, context 1              

When creating a future by using async, we are:

  1. copying all the arguments to the new context,
  2. creating a brand new stack and scopes,
  3. finally create a separate thread.

This forbids any sort of synchronization between threads since ArkScript does not expose references or any kind of lock that could be shared (this was done for simplicity reasons, as the language aims to be somewhat minimalist but still usable).

However this approach isn't better (nor worse) than Python's, as we create a new thread per call, and the number of threads per CPU is limited, which is a bit costly. Luckily I don't see that as problem to tackle, as one should never create hundreds or thousands of threads simultaneously nor call hundreds or thousands of async Python functions simultaneously: both would result in a huge slow down of your program.

In the first case, this would slowdown your process (even computer) as the OS is juggling to give time to every thread ; in the second case it is Python's scheduler that would have to juggle between all of your coroutines.

[!NOTE]
Out of the box, ArkScript does not provide mechanisms for thread synchronization, but even if we pass a UserType (which is a wrapper on top of type-erased C++ objects) to a function, the underlying object isn't copied.

With some careful coding, one could create a lock using the UserType construct, that would allow synchronization between threads.

(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)

Conclusion

ArkScript and Python use two very different kinds of async / await: the first one requires the use of async at the call site and spawns a new thread with its own context, while the latter requires the programmer to mark functions as async to be able to use await, and those async functions are coroutines, running in the same thread as the interpreter.

Sources

  1. Stack Exchange — Why was Python written with the GIL?
  2. Python Wiki — GlobalInterpreterLock
  3. stuffwithstuff - What color is your function?

Originally from lexp.lt

Das obige ist der detaillierte Inhalt vonVergleich der asynchronen Modelle Python und ArkScript. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn