Python asynchrone: diverses formes de compétition

Avec l'avènement de Python 3, il y a pas mal de buzz sur «asynchronisme» et «simultanéité», nous pouvons supposer que Python a récemment introduit ces fonctionnalités / concepts. Mais ce n'est pas le cas. Nous avons utilisé ces opérations à plusieurs reprises. De plus, les débutants pourraient penser qu'asyncio est le seul ou le meilleur moyen de recréer et d'utiliser des opérations asynchrones / parallèles. Dans cet article, nous examinerons différentes façons de réaliser le parallélisme, leurs avantages et leurs inconvénients.

Définition des termes:


Avant de nous plonger dans les aspects techniques, il est important d'avoir une compréhension de base des termes souvent utilisés dans ce contexte.

Synchrone et asynchrone:

Dans les opérations synchrones , les tâches sont effectuées l'une après l'autre. En asynchrone, les tâches peuvent être démarrées et terminées indépendamment les unes des autres. Une tâche asynchrone peut démarrer et continuer à s'exécuter pendant que l'exécution se déplace vers une nouvelle tâche. Les tâches asynchrones ne bloquent pas (ne forcent pas à attendre la fin de la tâche) les opérations et sont généralement effectuées en arrière-plan.

Par exemple, vous devez contacter une agence de voyages pour planifier vos prochaines vacances. Vous devez envoyer une lettre à votre superviseur avant de prendre l'avion. En mode synchrone, vous appelez d'abord l'agence de voyages, et si on vous demande d'attendre, vous attendez qu'ils vous répondent. Ensuite, vous commencerez à écrire une lettre au leader. Ainsi, vous effectuez les tâches l'une après l'autre. [exécution synchrone, env. traducteur] Mais, si vous êtes intelligent, ils vous ont demandé d'attendre [raccrocher au téléphone, env. traducteur] vous commencerez à rédiger un e-mail et lorsque vous parlerez à nouveau, vous mettrez un terme à l'écriture, parlez, puis ajouterez la lettre. Vous pouvez également demander à un ami d'appeler l'agence et d'écrire une lettre vous-même. C'est asynchrone, les tâches ne se bloquent pas.

Compétitivité et concurrence:

La compétitivité implique que deux tâches sont exécutées conjointement . Dans notre exemple précédent, lorsque nous avons considéré l'exemple asynchrone, nous avons progressivement progressé dans l'écriture d'une lettre, puis dans une conversation avec une tournée. agence. C'est ça la compétitivité .

Lorsque nous avons demandé à appeler un ami et que nous avons écrit une lettre nous-mêmes, les tâches ont été effectuées en parallèle .

La concurrence est essentiellement une forme de concurrence. Mais la concurrence dépend du matériel. Par exemple, si la CPU n'a qu'un seul cœur, deux tâches ne peuvent pas être exécutées en parallèle. Ils partagent simplement le temps processeur entre eux. Ensuite, c'est la concurrence, mais pas la concurrence. Mais lorsque nous avons plusieurs cœurs [en tant qu'ami dans l'exemple précédent, qui est le deuxième noyau, env. traducteur] nous pouvons effectuer plusieurs opérations (selon le nombre de cœurs) en même temps.

Pour résumer:

  • Synchronisation: bloque les opérations (blocage)
  • Asynchronie: ne bloque pas les opérations (non bloquantes)
  • Compétitivité: progrès conjoint (conjoint)
  • Concurrence: progression parallèle (parallèle)

La concurrence implique la concurrence. Mais la concurrence n'implique pas toujours la concurrence.

Fils et processus


Python prend en charge les threads depuis très longtemps. Les threads vous permettent d'effectuer des opérations de manière compétitive. Mais il y a un problème avec Global Interpreter Lock (GIL) en raison des threads qui ne peuvent pas fournir une véritable concurrence. Et pourtant, avec l'avènement du multitraitement, vous pouvez utiliser plusieurs cœurs à l'aide de Python.

Fils

Prenons un petit exemple. Dans le code suivant, la fonction de travail s'exécutera sur plusieurs threads de manière asynchrone et simultanée.

import threading import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = threading.Thread(target=worker, args=(i,)) t.start() print("All Threads are queued, let's see when they finish!") 

Et voici un exemple de sortie:

 $ python thread_test.py All Threads are queued, let's see when they finish! I am Worker 1, I slept for 1 seconds I am Worker 3, I slept for 4 seconds I am Worker 4, I slept for 5 seconds I am Worker 2, I slept for 7 seconds I am Worker 0, I slept for 9 seconds 

Ainsi, nous avons démarré 5 threads pour la collaboration et après leur démarrage (c'est-à-dire après le lancement de la fonction de travail), l'opération n'attend pas la fin des threads avant de passer à la prochaine instruction d'impression. Il s'agit d'une opération asynchrone.

Dans notre exemple, nous avons passé la fonction au constructeur Thread. Si nous le voulions, nous pourrions implémenter une sous-classe avec une méthode (style OOP).

Lectures complémentaires:

Pour en savoir plus sur les flux, utilisez le lien ci-dessous:


Verrou d'interprète global (GIL)

GIL a été introduit pour faciliter la gestion de la mémoire de CPython et fournir la meilleure intégration avec C (par exemple avec des extensions). GIL est un mécanisme de verrouillage lorsque l'interpréteur Python n'exécute qu'un seul thread à la fois. C'est-à-dire un seul thread peut être exécuté en bytecode Python à la fois. GIL garantit que plusieurs threads ne sont pas exécutés en parallèle .

Détails rapides de GIL:

  • Un thread peut s'exécuter à la fois.
  • L'interpréteur Python bascule entre les threads pour atteindre la compétitivité.
  • GIL est applicable à CPython (implémentation standard). Mais comme, par exemple, Jython et IronPython n'ont pas GIL.
  • GIL accélère les programmes monothread.
  • GIL n'interfère généralement pas avec les E / S.
  • GIL facilite l'intégration de bibliothèques thread-safe dans C, grâce à GIL, nous avons de nombreuses extensions / modules haute performance écrits en C.
  • Pour les tâches dépendantes du processeur, l'interpréteur vérifie tous les N ticks et bascule les threads. Par conséquent, un thread ne bloque pas les autres.

Beaucoup voient GIL comme une faiblesse. Je considère cela comme une bénédiction, car des bibliothèques telles que NumPy, SciPy ont été créées, qui occupent une position spéciale et unique dans la communauté scientifique.

Lectures complémentaires:

Ces ressources vous permettront de vous plonger dans le GIL:


Processus

Pour atteindre la simultanéité en Python, un module multitraitement a été ajouté qui fournit une API et ressemble beaucoup si vous avez utilisé le threading auparavant.

Allons simplement changer l'exemple précédent. Maintenant, la version modifiée utilise le processus au lieu du flux .

 import multiprocessing import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = multiprocessing.Process(target=worker, args=(i,)) t.start() print("All Processes are queued, let's see when they finish!") 

Qu'est-ce qui a changé? Je viens d'importer le module multitraitement au lieu de threader . Et puis, au lieu d'un fil, j'ai utilisé un processus. C'est tout! Maintenant, au lieu de nombreux threads, nous utilisons des processus qui s'exécutent sur différents cœurs de processeur (à moins, bien sûr, que votre processeur ait plusieurs cœurs).

En utilisant la classe Pool, nous pouvons également répartir l'exécution d'une fonction entre plusieurs processus pour différentes valeurs d'entrée. Un exemple des documents officiels:

 from multiprocessing import Pool def f(x): return x*x if __name__ == '__main__': p = Pool(5) print(p.map(f, [1, 2, 3])) 

Ici, au lieu d'itérer sur la liste de valeurs et d'appeler la fonction f une à la fois, nous exécutons en fait la fonction dans différents processus. Un processus fait f (1), l'autre f (2) et l'autre f (3). Enfin, les résultats sont à nouveau combinés dans une liste. Cela nous permet de décomposer les calculs lourds en parties plus petites et de les exécuter en parallèle pour un calcul plus rapide.

Lectures complémentaires:


Module Concurrent.futures

Le module concurrent.futures est volumineux et facilite l'écriture de code asynchrone très facilement. Mes favoris sont ThreadPoolExecutor et ProcessPoolExecutor . Ces artistes prennent en charge un pool de threads ou de processus. Nous envoyons nos tâches au pool, et il exécute les tâches dans un thread / processus accessible. Un objet Future est renvoyé qui peut être utilisé pour interroger et récupérer le résultat une fois la tâche terminée.

Et voici un exemple ThreadPoolExecutor:

 from concurrent.futures import ThreadPoolExecutor from time import sleep def return_after_5_secs(message): sleep(5) return message pool = ThreadPoolExecutor(3) future = pool.submit(return_after_5_secs, ("hello")) print(future.done()) sleep(5) print(future.done()) print(future.result()) 

J'ai un article sur concurrent.futures masnun.com/2016/03/29/python-a-quick-introduction-to-the-concurrent-futures-module.html . Il peut être utile pour une étude plus approfondie de ce module.

Lectures complémentaires:


Asyncio - quoi, comment et pourquoi?


Vous avez probablement une question que beaucoup de gens de la communauté Python ont - qu'est-ce que l'asyncio apporte de nouveau? Pourquoi y avait-il une autre façon d'utiliser les E / S asynchrones? N'avions-nous pas déjà des fils et des processus? Voyons voir!

Pourquoi avons-nous besoin d'asyncio?

Les processus sont très coûteux [en termes de consommation de ressources, env. traducteur] pour créer. Par conséquent, pour les opérations d'E / S, les threads sont principalement sélectionnés. Nous savons que les E / S dépendent de facteurs externes - des disques lents ou des décalages réseau désagréables rendent les E / S souvent imprévisibles. Supposons maintenant que nous utilisons des threads pour les E / S. 3 threads effectuent diverses tâches d'E / S. L'interprète devrait basculer entre les flux concurrentiels et donner à chacun un certain temps à tour de rôle. Appelez les flux T1, T2 et T3. Trois threads ont commencé leur opération d'E / S. T3 le termine en premier. T2 et T1 attendent toujours des E / S. L'interpréteur Python passe en T1, mais il attend toujours. Eh bien, l'interpréteur passe à T2, et l'interprète attend toujours, puis passe à T3, qui est prêt et exécute le code. Voyez-vous cela comme un problème?

T3 était prêt, mais l'interprète est d'abord passé de T2 à T1 - cela a entraîné des coûts de commutation, que nous aurions pu éviter si l'interprète était d'abord passé à T3, non?

Qu'est-ce que l'asynio?

Asyncio nous fournit une boucle d'événements avec d'autres trucs sympas. La boucle d'événements surveille les événements d'E / S et commute les tâches qui sont prêtes et en attente d'opérations d'E / S [la boucle d'événements est une construction logicielle qui attend l'arrivée et envoie des événements ou des messages dans le programme, env. traducteur] .

L'idée est très simple. Il y a une boucle d'événement. Et nous avons des fonctions qui effectuent des E / S asynchrones. Nous transférons nos fonctions dans la boucle d'événements et lui demandons de les exécuter pour nous. La boucle d'événement nous renvoie un objet Future, comme une promesse que dans le futur nous obtiendrons quelque chose. Nous tenons une promesse, vérifions de temps en temps si cela importe (nous ne pouvons vraiment pas attendre), et enfin, lorsque la valeur est reçue, nous l'utilisons dans d'autres opérations [c.-à-d. nous avons envoyé une demande, on nous a immédiatement donné un ticket et on nous a dit d'attendre le résultat. Nous vérifions périodiquement le résultat et dès qu'il est reçu, nous prenons un ticket et obtenons une valeur dessus, env. traducteur] .

Asyncio utilise des générateurs et des coroutines pour arrêter et reprendre des tâches. Vous pouvez lire les détails ici:


Comment utiliser asyncio?

Avant de commencer, regardons un exemple:

 import asyncio import datetime import random async def my_sleep_func(): await asyncio.sleep(random.randint(0, 5)) async def display_date(num, loop): end_time = loop.time() + 50.0 while True: print("Loop: {} Time: {}".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await my_sleep_func() loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever() 

Notez que la syntaxe async / wait est uniquement pour Python 3.5 et versions ultérieures. Passons en revue le code:

  • Nous avons une fonction display_date asynchrone qui prend un nombre (comme identifiant) et une boucle d'événement comme paramètres.
  • La fonction a une boucle infinie, qui est interrompue après 50 secondes. Mais pendant cette période, elle imprime à plusieurs reprises le temps et fait une pause. La fonction wait peut attendre la fin des autres fonctions asynchrones (coroutine).
  • Nous passons la fonction à la boucle d'événement (en utilisant la méthode Ensure_future).
  • Nous commençons un cycle d'événements.

Chaque fois que wait est appelé, asyncio se rend compte que la fonction prendra probablement un certain temps. Ainsi, il suspend l'exécution, commence à surveiller tous les événements d'E / S qui lui sont associés et vous permet d'exécuter des tâches. Lorsque asyncio remarque que les E / S de la fonction en pause sont prêtes, il reprend la fonction.

Faire le bon choix.


Nous venons de traverser les formes de compétitivité les plus populaires. Mais la question demeure - que choisir? Cela dépend des cas d'utilisation. D'après mon expérience, j'ai tendance à suivre ce pseudo-code:

 if io_bound: if io_very_slow: print("Use Asyncio") else: print("Use Threads") else: print("Multi Processing") 

  • CPU Bound => Multi Processing
  • Liées E / S, E / S rapides, nombre de connexions limité => Multi-threading
  • E / S liées, E / S lentes, nombreuses connexions => Asyncio

[Remarque traducteur]

Source: https://habr.com/ru/post/fr421625/


All Articles