Exécuter des traitements concurrents
concurrent.futures : pools de traitements
| Élément | Usage |
|---|---|
ThreadPoolExecutor |
Pool de threads, utile notamment pour des attentes d'entrées-sorties. |
ProcessPoolExecutor |
Pool de processus, souvent adapté aux calculs Python intensifs. |
submit() |
Soumettre un appel et obtenir un Future. |
map() |
Appliquer une fonction à des éléments. |
as_completed() |
Parcourir les tâches terminées. |
future.result() |
Récupérer un résultat ou propager une exception. |
from concurrent.futures import ThreadPoolExecutor
def double(number):
return number * 2
with ThreadPoolExecutor(max_workers=3) as executor:
print(list(executor.map(double, [1, 2, 3]))) # [2, 4, 6]
Cet exemple illustre l'API ; un traitement aussi court ne justifie pas un pool. Avec des processus, les fonctions et données doivent pouvoir être transférées entre processus et le point d'entrée doit généralement être protégé par if __name__ == "__main__":.
Parallèle C# et .NET
int[] numbers = { 1, 2, 3 };
int[] doubled = numbers.AsParallel()
.AsOrdered()
.WithDegreeOfParallelism(3)
.Select(n => n * 2)
.ToArray();
Console.WriteLine(string.Join(", ", doubled)); // 2, 4, 6
PLINQ fournit ici une projection parallèle avec ordre conservé. Task.Run() et le pool de threads répondent à d’autres formes de soumission ; il n’existe pas de ProcessPoolExecutor standard identique. Sur CPython classique avec GIL, les threads ne parallélisent généralement pas les calculs Python purs ; les threads .NET n’ont pas cette contrainte globale. Le parallélisme reste inutile pour cette transformation minuscule.
asyncio : concurrence asynchrone
asyncio orchestre des coroutines qui rendent la main pendant leurs attentes. C'est utile lorsque les API appelées prennent elles-mêmes en charge l'asynchronisme.
| Fonction | Usage |
|---|---|
run() |
Exécuter une coroutine depuis un point d'entrée synchrone. |
sleep() |
Attendre sans bloquer le thread de la boucle. |
create_task() |
Planifier une coroutine. |
gather() |
Attendre plusieurs opérations et regrouper leurs résultats. |
to_thread() |
Déporter un appel bloquant dans un thread. |
import asyncio
async def get_value(value):
await asyncio.sleep(0.01)
return value
async def main():
results = await asyncio.gather(get_value(10), get_value(20))
print(results) # [10, 20]
if __name__ == "__main__":
asyncio.run(main())
Cet exemple s'exécute comme script. Dans un notebook disposant déjà d'une boucle, utilisons plutôt await main(). L'asynchronisme ne rend pas automatiquement un calcul intensif plus rapide.
Parallèle C# et .NET
static async Task<int> GetValueAsync(int value)
{
await Task.Delay(10);
return value;
}
int[] results = await Task.WhenAll(GetValueAsync(10), GetValueAsync(20));
Console.WriteLine(string.Join(", ", results)); // 10, 20
Task.Delay() exprime l’attente non bloquante et Task.WhenAll() regroupe les résultats dans l’ordre des tâches fournies. Une méthode C# async commence généralement à s’exécuter dès son appel ; appeler une fonction Python async def crée une coroutine qu’il faut attendre ou planifier. WhenAll() attend toutes les tâches ; gather() avec ses options par défaut peut propager une exception avant la fin des autres opérations.