
concurrent.futures est l'API haut niveau de Python pour lancer des tâches en parallèle sans manipuler directement les threads ni les processus. Vous soumettez du travail à un exécuteur (ThreadPoolExecutor ou ProcessPoolExecutor), et il vous rend des objets Future qui représentent un résultat à venir. La même interface couvre deux besoins opposés : les threads pour le code I/O bound (réseau, disque, API) et les processus pour le code CPU bound (calcul intensif). Ce guide couvre submit, map, as_completed, wait, la gestion des exceptions dans un Future et le choix entre les deux exécuteurs.
Il s'adresse aux développeurs déjà à l'aise avec les fonctions et la gestion des exceptions, qui veulent paralléliser proprement sans réinventer une file de tâches. Les exemples ont été exécutés avec Python 3.12.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Soumettre des tâches avec
submitet récupérer un objetFuture. - Utiliser
mappour appliquer une fonction à une séquence en parallèle. - Traiter les résultats au fil de l'eau avec
as_completedetwait. - Récupérer les exceptions levées dans une tâche via
.result(). - Choisir
ThreadPoolExecutor(I/O bound) ouProcessPoolExecutor(CPU bound), mesures à l'appui.
Pourquoi préférer cette API à threading et multiprocessing bruts
Section intitulée « Pourquoi préférer cette API à threading et multiprocessing bruts »Les modules threading et multiprocessing sont les briques bas niveau de la concurrence. Ils fonctionnent, mais ils vous laissent gérer à la main la création des threads, leur jonction (join), la collecte des résultats dans une structure partagée et la remontée des erreurs. Chaque programme concurrent finit par réécrire la même plomberie fragile, souvent avec des bugs de synchronisation à la clé.
concurrent.futures remplace toute cette mécanique par un modèle unique : un pool de workers réutilisables et un objet Future par tâche. L'exécuteur recycle les workers au lieu d'en créer un par tâche, il collecte les résultats pour vous et il propage les exceptions au moment où vous lisez le résultat. Vous écrivez la logique métier, pas la tuyauterie.
L'atout décisif est l'uniformité. Passer d'une exécution par threads à une exécution par processus se résume à changer le nom de la classe instanciée : ThreadPoolExecutor devient ProcessPoolExecutor, et le reste du code ne bouge pas. Cette symétrie rend l'expérimentation triviale, là où threading et multiprocessing exposent des API différentes.
Deux exécuteurs, une seule interface
Section intitulée « Deux exécuteurs, une seule interface »Un exécuteur (executor) est un gestionnaire de pool : il maintient un nombre fixe de workers et leur distribue les tâches que vous soumettez. La bibliothèque en fournit deux, qui partagent exactement la même interface mais reposent sur des mécanismes très différents.
Le ThreadPoolExecutor lance des threads dans le processus courant. Les threads partagent la mémoire, se créent vite et sont parfaits pour du travail qui attend beaucoup (réseau, fichiers, base de données). Le ProcessPoolExecutor lance des processus séparés, chacun avec son propre interpréteur : il contourne le GIL (le verrou global de l'interpréteur, qui empêche deux threads d'exécuter du bytecode Python en même temps) et exploite réellement plusieurs cœurs pour du calcul.
Les deux s'utilisent comme gestionnaires de contexte avec with, ce qui garantit un arrêt propre du pool à la sortie du bloc.
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
with ThreadPoolExecutor(max_workers=4) as executor: ... # le pool est fermé automatiquement en sortie de bloc
with ProcessPoolExecutor(max_workers=4) as executor: ... # meme code, autre moteur d'executionsubmit et les objets Future
Section intitulée « submit et les objets Future »La méthode submit place une fonction dans la file du pool et rend immédiatement un objet Future, sans attendre le calcul. Ce Future est une promesse : un conteneur qui recevra plus tard soit un résultat, soit une exception. Vous lisez sa valeur avec .result(), qui bloque jusqu'à ce que la tâche soit terminée.
from concurrent.futures import ThreadPoolExecutor, as_completed
def carre(n: int) -> int: return n * n
with ThreadPoolExecutor(max_workers=3) as executor: # submit renvoie un Future tout de suite, sans bloquer futures = {executor.submit(carre, n): n for n in range(1, 6)}
for future in as_completed(futures): entree = futures[future] print(f"carre({entree}) = {future.result()}")Ce motif du dictionnaire Future vers argument est un idiome central : il permet de retrouver l'entrée associée à chaque résultat, car l'ordre d'arrivée n'est pas garanti. La sortie réelle le montre, les tâches ne reviennent pas dans l'ordre de soumission :
carre(4) = 16carre(5) = 25carre(3) = 9carre(1) = 1carre(2) = 4Un objet Future offre plusieurs méthodes utiles au-delà de .result() : .done() indique si la tâche est finie, .running() si elle s'exécute, .cancel() tente de l'annuler tant qu'elle n'a pas démarré, et .exception() renvoie l'erreur éventuelle sans la relever. On les combine rarement au quotidien, mais ils rendent le suivi fin possible.
map : le raccourci quand on itère
Section intitulée « map : le raccourci quand on itère »Quand vous appliquez la même fonction à chaque élément d'une séquence, executor.map est plus direct que submit. Il fonctionne comme le map intégré, mais distribue les appels sur le pool et renvoie les résultats dans l'ordre des entrées, pas dans l'ordre de fin.
with ThreadPoolExecutor(max_workers=5) as executor: resultats = list(executor.map(recuperer, services))La différence de contrat entre les deux approches est importante. map préserve l'ordre et relance chaque exception au moment où vous itérez sur le résultat correspondant ; c'est le bon choix quand la correspondance entrée/sortie compte et que vous voulez un code compact. submit plus as_completed traite les résultats dès qu'ils arrivent, ce qui est préférable quand certaines tâches sont bien plus longues que d'autres et que vous ne voulez pas attendre la plus lente pour commencer à travailler.
as_completed : traiter les résultats au fil de l'eau
Section intitulée « as_completed : traiter les résultats au fil de l'eau »La fonction as_completed prend un ensemble de Future et renvoie un itérateur qui produit chaque Future au moment précis où il se termine. C'est l'outil idéal pour afficher une progression ou réagir aux premiers résultats sans attendre les traînards. On l'a déjà vu à l'œuvre plus haut avec le dictionnaire de correspondance.
Son intérêt est la réactivité. Sur un lot de requêtes où une seule est lente, as_completed vous laisse traiter les neuf réponses rapides pendant que la dixième arrive encore. Combiné au dictionnaire Future vers argument, il conserve le lien entre chaque résultat et sa donnée d'origine malgré le désordre d'arrivée.
wait : attendre selon une condition
Section intitulée « wait : attendre selon une condition »Là où as_completed itère sur les tâches finies, wait rend la main une fois selon une condition que vous choisissez, et renvoie deux ensembles : les Future terminés et ceux encore en cours. Le paramètre return_when pilote le comportement : ALL_COMPLETED (par défaut) attend tout le monde, FIRST_COMPLETED rend la main dès la première fin, FIRST_EXCEPTION s'arrête à la première erreur.
import timefrom concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED
def tache(nom: str, duree: float) -> str: time.sleep(duree) return f"{nom} terminee"
with ThreadPoolExecutor(max_workers=3) as executor: futures = [ executor.submit(tache, "rapide", 0.2), executor.submit(tache, "moyenne", 0.5), executor.submit(tache, "lente", 1.0), ] termines, en_cours = wait(futures, return_when=FIRST_COMPLETED) print(f"Termines : {len(termines)}, en cours : {len(en_cours)}") for f in termines: print(f.result())À l'exécution, wait s'est débloqué dès la fin de la tâche la plus courte et a laissé les deux autres tourner :
Termines : 1, en cours : 2rapide termineeCe découpage terminés / en cours sert quand vous devez prendre une décision globale à un instant donné, par exemple appliquer un délai maximal avec le paramètre timeout et annuler ce qui traîne encore. Pour un simple traitement séquentiel des résultats, as_completed reste plus lisible.
Gérer les exceptions dans un Future
Section intitulée « Gérer les exceptions dans un Future »Le point le plus souvent mal compris : une exception levée dans une tâche ne remonte pas au moment où elle se produit. Elle est capturée et stockée dans le Future, puis relevée seulement quand vous appelez .result(). C'est un comportement voulu, car la tâche s'exécute dans un autre thread ou un autre processus : Python ne peut pas propager l'erreur ailleurs qu'au point de lecture.
from concurrent.futures import ThreadPoolExecutor
def diviser(n: int) -> float: return 10 / n
with ThreadPoolExecutor() as executor: future = executor.submit(diviser, 0) try: resultat = future.result() # releve l'exception ici except ZeroDivisionError as erreur: print(f"Erreur attrapee : {erreur}") print("Le programme continue")L'exécution confirme que l'erreur est bien relevée au .result() et non à la soumission, ce qui permet de l'attraper localement :
Erreur attrapee : division by zeroLe programme continueThreadPoolExecutor : accélérer un code I/O bound
Section intitulée « ThreadPoolExecutor : accélérer un code I/O bound »Le ThreadPoolExecutor brille quand vos tâches passent l'essentiel de leur temps à attendre : appels réseau, requêtes HTTP, lectures disque, interrogations de base de données. Pendant qu'un thread attend une réponse, le GIL est relâché et les autres threads progressent. Les attentes se chevauchent au lieu de s'empiler.
L'exemple simule cinq appels de services, chacun bloquant une seconde. En version séquentielle, on attend cinq secondes ; avec un pool de cinq threads, les attentes se superposent.
import timefrom concurrent.futures import ThreadPoolExecutor
def recuperer(nom: str) -> str: # Simule un appel reseau lent (API, base de donnees...) time.sleep(1) return f"{nom} : 200 OK"
services = ["auth", "facturation", "stock", "notifications", "recherche"]
debut = time.perf_counter()sequentiel = [recuperer(s) for s in services]print(f"Sequentiel : {time.perf_counter() - debut:.2f} s")
debut = time.perf_counter()with ThreadPoolExecutor(max_workers=5) as executor: concurrent = list(executor.map(recuperer, services))print(f"Concurrent : {time.perf_counter() - debut:.2f} s")Le gain mesuré est net, cinq secondes tombent à une seconde :
Sequentiel : 5.00 sConcurrent : 1.00 sCe facteur cinq correspond exactement au nombre d'attentes menées de front. Sur du vrai trafic réseau, le gain dépend de la latence et du nombre de workers, mais le principe tient : plus vous avez d'attentes indépendantes, plus les threads les recouvrent efficacement. Pour un cas concret, ce motif accélère un client requests qui interroge de nombreux points d'API.
ProcessPoolExecutor : paralléliser un calcul CPU bound
Section intitulée « ProcessPoolExecutor : paralléliser un calcul CPU bound »Face à un calcul intensif, les threads ne servent à rien : le GIL interdit à deux threads d'exécuter du bytecode Python simultanément, donc un pool de threads sur du CPU pur ne va pas plus vite. Il faut de vrais processus, et c'est le rôle du ProcessPoolExecutor, qui répartit le travail sur plusieurs cœurs.
L'exemple compte les nombres premiers sous un seuil, quatre fois, sur un pool de quatre processus. La fonction est volontairement gourmande en CPU pour rendre l'écart visible.
import timefrom concurrent.futures import ProcessPoolExecutor
def compter_premiers(limite: int) -> int: # Calcul volontairement intensif (CPU bound) total = 0 for n in range(2, limite): est_premier = True for d in range(2, int(n ** 0.5) + 1): if n % d == 0: est_premier = False break if est_premier: total += 1 return total
taches = [500_000, 500_000, 500_000, 500_000]
if __name__ == "__main__": debut = time.perf_counter() sequentiel = [compter_premiers(n) for n in taches] print(f"Sequentiel : {time.perf_counter() - debut:.2f} s -> {sequentiel}")
debut = time.perf_counter() with ProcessPoolExecutor(max_workers=4) as executor: parallele = list(executor.map(compter_premiers, taches)) print(f"Parallele : {time.perf_counter() - debut:.2f} s -> {parallele}")Sur une machine à plusieurs cœurs, les quatre calculs s'exécutent en même temps et le temps total s'effondre :
Sequentiel : 1.98 s -> [41538, 41538, 41538, 41538]Parallele : 0.62 s -> [41538, 41538, 41538, 41538]Le passage de 1,98 s à 0,62 s montre l'accélération réelle permise par quatre processus. Le facteur n'est pas exactement quatre : la création des processus, la sérialisation des arguments et le rapatriement des résultats coûtent un peu. Ce surcoût est le prix des processus, rentable uniquement quand le calcul lui-même est long devant la mise en place.
Choisir : ThreadPoolExecutor ou ProcessPoolExecutor
Section intitulée « Choisir : ThreadPoolExecutor ou ProcessPoolExecutor »Le choix ne dépend que d'une question : votre programme est-il limité par l'attente ou par le calcul ? La réponse détermine l'exécuteur, et se tromper annule tout le bénéfice de la parallélisation.
| Votre tâche est limitée par... | Exécuteur | Pourquoi |
|---|---|---|
| des entrées-sorties (réseau, disque, API, base) | ThreadPoolExecutor | le GIL est relâché pendant l'attente, les threads se recouvrent |
| un calcul intensif (maths, images, parsing lourd) | ProcessPoolExecutor | des processus séparés contournent le GIL et saturent les cœurs |
En pratique, on essaie d'abord les threads : ils sont plus légers, sans sérialisation ni garde __main__, et couvrent la majorité des besoins réels, largement dominés par les I/O. On ne passe aux processus que lorsqu'un profilage confirme que le programme est CPU bound et que le calcul est assez long pour amortir le surcoût. Pour de la concurrence I/O à très grande échelle sur un seul thread, asyncio reste une alternative complémentaire aux pools de threads.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »concurrent.futuresest l'API haut niveau de concurrence : un pool de workers et unFuturepar tâche, sans plomberie manuelle.submitrend unFutureimmédiatement ;.result()bloque jusqu'au résultat et relève l'exception éventuelle.mapapplique une fonction à une séquence, dans l'ordre des entrées ;as_completedlivre les résultats dès qu'ils arrivent.waitrend la main selonreturn_when(ALL_COMPLETED,FIRST_COMPLETED,FIRST_EXCEPTION) et sépare terminés et en cours.- Une exception dans une tâche est stockée dans le
Futureet relevée au.result(), jamais au moment où elle survient. ThreadPoolExecutorpour l'I/O bound : cinq attentes d'une seconde tombent à une seconde.ProcessPoolExecutorpour le CPU bound : quatre calculs passent de 1,98 s à 0,62 s sur quatre processus.- Le code
ProcessPoolExecutorvit sousif __name__ == "__main__":et ne manipule que des données sérialisables.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- asyncio en Python : l'approche mono-thread de la concurrence I/O, complémentaire des pools de threads pour des milliers d'attentes.
- Gérer les exceptions : maîtriser la remontée d'erreurs, indispensable pour lire correctement un
Future. - Appeler des API avec requests : le cas d'usage I/O bound typique où un
ThreadPoolExecutoraccélère un lot d'appels. - asyncio avancé : l'approche mono-thread pour des milliers d'attentes concurrentes, à comparer aux pools de threads.
- Profiler son code : mesurer le gain réel d'un pool avant de conclure qu'il accélère votre traitement.