Maison  >  Article  >  développement back-end  >  Comparaison des modèles asynchrones Python et ArkScript

Comparaison des modèles asynchrones Python et ArkScript

WBOY
WBOYoriginal
2024-09-04 06:51:54740parcourir

Comparing Python and ArkScript asynchronous models

Python a reçu beaucoup d'attention ces derniers temps. La version 3.13, prévue pour octobre de cette année, lancera l'énorme travail de suppression du GIL. Une avant-première est déjà disponible pour les utilisateurs curieux qui souhaitent essayer un Python (presque) sans GIL.

Tout ce battage médiatique m'a fait creuser dans mon propre langage, ArkScript, car j'avais aussi un Global VM Lock dans le passé (ajouté dans la version 3.0.12, en 2020, supprimé dans la version 3.1.3 en 2022), pour comparer les choses et me forcer à approfondir le comment et le pourquoi du Python GIL.

Définitions

  1. Pour commencer, définissons ce qu'est un GIL (Verrouillage global de l'interprète) :

Un verrou d'interpréteur global (GIL) est un mécanisme utilisé dans les interpréteurs de langage informatique pour synchroniser l'exécution des threads afin qu'un seul thread natif (par processus) puisse exécuter des opérations de base (telles que l'allocation de mémoire et le comptage de références) à la fois. temps.

Wikipédia — Verrouillage global de l'interprète

  1. La concurrence se produit lorsque deux tâches ou plus peuvent démarrer, s'exécuter et se terminer dans des périodes de temps qui se chevauchent, mais cela ne signifie pas qu'elles s'exécuteront toutes les deux simultanément.

  2. Le
  3. Le parallélisme se produit lorsque les tâches s'exécutent littéralement en même temps, par exemple sur un processeur multicœur.

Pour une explication détaillée, consultez cette réponse Stack Overflow.

Le GIL de Python

Le GIL peut augmenter la vitesse des programmes monothread car vous n'avez pas besoin d'acquérir et de libérer des verrous sur toutes les structures de données : l'intégralité de l'interpréteur est verrouillée, vous êtes donc en sécurité par défaut.

Cependant, comme il y a un GIL par interpréteur, cela limite le parallélisme : vous devez générer un tout nouvel interpréteur dans un processus séparé (en utilisant le module multitraitement au lieu du threading) pour utiliser plus d'un cœur ! Cela coûte plus cher que la simple création d'un nouveau thread, car vous devez maintenant vous soucier de la communication inter-processus, ce qui ajoute une surcharge non négligeable (voir GeekPython — GIL devient facultatif dans Python 3.13 pour les benchmarks).

Comment cela affecte-t-il l'asynchrone de Python ?

Dans le cas de Python, cela tient au fait que l'implémentation principale, CPython, ne dispose pas d'une gestion de la mémoire thread-safe. Sans le GIL, le scénario suivant générerait une condition de concurrence :

  1. créer une variable partagée nombre = 5
  2. thread 1 : compte *= 2
  3. fil 2 : compte += 1

Si le thread 1 s'exécute en premier, le compte sera 11 (compte * 2 = 10, puis compte + 1 = 11).

Si le thread 2 s'exécute en premier, le compte sera 12 (compte + 1 = 6, puis compte * 2 = 12).

L'ordre d'exécution est important, mais pire encore peut arriver : si les deux threads lisent le compte en même temps, l'un effacera le résultat de l'autre, et le compte sera soit 10, soit 6 !

Dans l'ensemble, avoir un GIL rend la mise en œuvre (CPython) plus facile et plus rapide dans les cas généraux :

  • plus rapide dans le cas à thread unique (pas besoin d'acquérir/libérer un verrou pour chaque opération)
  • plus rapide dans le cas multithread pour les programmes liés aux IO (car ceux-ci se produisent en dehors du GIL)
  • plus rapide dans le cas multithread pour les programmes liés au processeur qui effectuent leur travail de calcul intensif en C (car le GIL est publié avant d'appeler le code C)

Cela facilite également l'encapsulation des bibliothèques C, car la sécurité des threads est garantie grâce au GIL.

L'inconvénient est que votre code est asynchrone comme en concurrent, mais pas parallèle.

[!NOTE]
Python 3.13 supprime le GIL !

Le PEP 703 a ajouté une configuration de construction --disable-gil afin que lors de l'installation de Python 3.13+, vous puissiez bénéficier d'améliorations de performances dans les programmes multithread.

Modèle Python asynchrone/attente

En Python, les fonctions doivent prendre une couleur : elles sont soit "normales", soit "async". Qu'est-ce que cela signifie en pratique ?

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

Étant donné qu'une fonction asynchrone ne renvoie pas de valeur immédiatement, mais appelle plutôt une coroutine, nous ne pouvons pas les utiliser partout comme rappels, à moins que la fonction que nous appelons ne soit conçue pour accepter des rappels asynchrones.

Nous obtenons une hiérarchie de fonctions, car les fonctions "normales" doivent être rendues asynchrones pour utiliser le mot-clé wait, nécessaire pour appeler des fonctions asynchrones :

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

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

En dehors de faire confiance à l'appelant, il n'y a aucun moyen de savoir si un rappel est asynchrone ou non (à moins que vous n'essayiez d'abord de l'appeler dans un bloc try/sauf pour vérifier une exception, mais c'est moche).

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn