PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

En Python, les threads et l’asyncio s’utilisent ensemble 8

mercredi 13 janvier 2016 à 13:11

En Python, les threads et l’asyncio s’utilisent ensemble

Je vois beaucoup dans les tutos ici et là des gens qui opposent asyncio avec l’ancienne manière de faire de l’IO non bloquante : les threads.

C’est une erreur : les deux méthodes ne sont pas opposées, elles sont complémentaires.

Faire des requêtes pendant que ça bloque

En effet, asyncio ne peut être non bloquant que sur le réseau : ça ne gère pas l’IO sur les fichiers. Par ailleurs, toute opération CPU bloque également la boucle d’évènement.

C’est très bien de ne pas bloquer sur le réseau, mais encore faut-il pouvoir faire la requête réseau.

Si votre programme est bloqué à attendre une compression zip qui dure 3 secondes, pendant ce temps, les opérations réseaux déjà lancées tournent bien.

MAIS IL N’EN LANCERA PAS DE NOUVELLES.

Pendant 3 secondes, aucune nouvelle requête ne sera faite puisque le programme ne fait qu’une chose : ziper. Ca marche pour d’autres choses hein : copie de fichier, gros calcul matheux, traverser une grande liste, etc.

Donc si vous avez fini toutes vos requêtes à la seconde 1, pendant 2 secondes, votre programme n’est pas utilisé à son plein potentiel.

Les threads ne permettent pas d’avancer plus vite, mais ils permettent de faire 2 travaux en parallèle. Par exemple, de lancer pendant ces 2 secondes des requêtes supplémentaires sur le réseau.

Asyncio a de l’overhead

On a vendu asyncio comme plus léger que les threads. Ce n’est pas tout à fait exact. asyncio a les avantages suivants :

Mais si on a quelques threads, le changement de contexte entre les threads est moins important que ce que coûte asyncio à faire tourner.

Pour certains travaux où le coût d’une opération réseau est faible, mais que cumulativement toutes les opérations ralentissent votre programme, un thread sera plus performant.

Par exemple, beaucoup d’opérations sur les bases de données tombent dans cette catégorie, à moins d’avoir des très longues requêtes.

Dans ce cas, avoir un thread dédié aux opérations de la base de données peut être une bonne décision.

Quoi faire ?

Si vous avez des opérations sur des fichiers ou de grosses opérations CPU, les faire travailler dans un thread peut booster votre programme. Ca tombe bien il y a une lib pour ça.

Si vous avez beaucoup de petites opérations réseau, les grouper, dans un thread à part, peut booster votre programme.

Et asyncio pour le reste, par exemple des requêtes HTTPs, DNS, SMTP, FTP, SSH, etc.

On peut donc copieusement utiliser les deux en parallèle. La bonne nouvelle, c’est que Guido a designé asyncio pour ça:

import asyncio
import aiohttp
 
async def main(loop):
 
    # ça c’est fait avec asyncio
    await aiohttp.get('http://bidule.com')
 
    # ça c’est fait dans un thread
    await loop.run_in_executor(None, gros_calcul, params)
 
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))

Notez bien :

Bien entendu, chaque fois que vous ajoutez un mécanisme de concurrence, vous ajoutez de la complexité, donc ne le faites que si c’est nécessaire.

Commencez avec un programme synchrone simple. Si c’est trop lent, ajouter les threads ou l’asyncio, si ça ne suffit pas, utilisez les deux.