1. Multithreading vs parallélisme
Multithreading : beaucoup, mais pas forcément en même temps
Le multithreading, c’est quand votre programme comporte plusieurs fils d’exécution. Chaque fil — comme une ligne d’action indépendante : l’un calcule quelque chose, un autre attend une saisie utilisateur, un troisième enregistre des données dans un fichier. En Java, vous créez des threads via la classe Thread, implémentez l’interface Runnable ou utilisez des outils de plus haut niveau comme ExecutorService (nous les verrons dans le prochain cours).
MAIS ! Le multithreading ne garantit pas que vos tâches s’exécutent réellement en même temps. Tout dépend du nombre de cœurs de votre processeur. Si vous n’avez qu’un seul cœur, les threads ne font que se « basculer » rapidement les uns avec les autres — si rapidement qu’il semble à l’humain que tout se passe simultanément. En réalité, le processeur n’exécute qu’un seul thread à un instant donné, les autres attendent leur tour.
Parallélisme : quand les tâches s’exécutent réellement en même temps
Le parallélisme, c’est lorsque votre code s’exécute véritablement en même temps sur plusieurs cœurs du processeur. Si vous avez un ordinateur moderne avec 4, 8, 16 cœurs — vous pouvez réellement accélérer le traitement de grosses tâches en les découpant en parties indépendantes et en les répartissant sur les cœurs.
Par analogie, le multithreading — c’est quand vous avez un seul cuisinier qui bascule rapidement entre la préparation du borchtch, la cuisson des côtelettes et la découpe de la salade. Le parallélisme — c’est quand vous avez plusieurs cuisiniers à la fois, et chacun est responsable de son plat.
Quelle est la différence en pratique ?
Le multithreading — c’est pour la commodité et la réactivité. Vous utilisez plusieurs threads pour que le programme ne « se fige » pas : un thread attend le réseau, un autre dessine l’interface, un troisième calcule quelque chose. Tout fonctionne en parallèle en apparence, mais pas nécessairement simultanément.
Le parallélisme — c’est pour la vitesse. Ici, plusieurs cœurs du processeur exécutent réellement différentes parties d’une tâche en même temps pour obtenir le résultat plus rapidement.
En d’autres termes : le multithreading aide à organiser le travail, et le parallélisme — à l’accélérer.
Important :
Le multithreading est nécessaire dès qu’il y a des tâches pouvant être effectuées indépendamment.
Le parallélisme est nécessaire lorsque vous souhaitez accélérer les calculs grâce à une répartition réelle du travail entre les cœurs.
Exemple : traitement d’un grand tableau
Imaginons que nous ayons un tableau de 10 millions de nombres et que nous voulions calculer la somme de tous les éléments.
Séquentiel :
Un thread parcourt tout le tableau et calcule la somme. Simple et fiable, mais long.
En multithreading (mais sur un seul cœur) :
Vous divisez le tableau en 4 parties, vous créez 4 threads, chacun calcule sa partie. Mais si vous n’avez qu’un seul cœur, les threads vont simplement travailler à tour de rôle — pas de gain, et les surcoûts de commutation de threads peuvent même ralentir le programme.
En parallèle (sur plusieurs cœurs) :
Vous divisez le tableau en 4 parties, vous lancez 4 threads, et chaque thread travaille réellement sur son cœur. La somme finale est composée des 4 parties. C’est réellement plus rapide — surtout sur de grands volumes de données.
En revanche, implémenter un traitement séquentiel de tableau est très simple, vous avez déjà écrit ce genre de programmes de nombreuses fois :
// Exemple : traitement séquentiel d’un tableau
int[] arr = new int[10_000_000];
// ... remplissage du tableau ...
long sum = 0;
for (int x : arr) {
sum += x;
}
System.out.println(sum);
Les versions multithread et parallèles — un peu plus complexes, nous les étudierons dans les prochains cours à l’aide d’outils modernes.
2. À quoi sert le parallélisme
Les processeurs modernes ont depuis longtemps dépassé le cadre d’un seul cœur. Même votre smartphone a probablement au moins quatre cœurs, et les ordinateurs de bureau et les serveurs — huit, seize, trente-deux et plus. Si une application sait utiliser tous ces cœurs, elle peut fonctionner beaucoup plus vite.
Autrefois, les performances des processeurs augmentaient grâce à la hausse de la fréquence — jusqu’au milieu des années 2000, cela fonctionnait réellement. Mais l’augmentation des fréquences a buté sur des limites physiques, et une nouvelle ère a commencé — systèmes multiprocesseurs et multicœurs. Désormais, les programmes qui gagnent sont ceux qui savent répartir efficacement le travail entre les cœurs.
Où le parallélisme apporte-t-il réellement une accélération ?
- Traitement de grandes quantités de données : analyse de journaux, statistiques, agrégation — tout ce qui peut être découpé en parties indépendantes.
- Rendu, traitement d’images et de vidéos : chaque pixel ou fragment peut être traité séparément.
- Calcul scientifique, modélisation : problèmes mathématiques, simulations, entraînement de modèles.
- Applications serveur : prise en charge simultanée de nombreux clients.
- Applications réactives : lorsqu’il faut réagir rapidement à de nombreux événements sans bloquer le thread principal.
Quand le parallélisme n’aide-t-il pas ?
- Si la tâche est petite, les surcoûts de lancement du parallélisme peuvent être supérieurs au gain.
- Si la tâche ne peut pas être découpée en parties indépendantes (par exemple, lorsque chaque étape dépend de la précédente).
- Lorsqu’il y a beaucoup de ressources partagées (par exemple, le même fichier), et que les threads commencent à se gêner mutuellement.
3. Tâches typiques pour le parallélisme
Voyons quelles tâches sont le plus souvent « distribuées » entre les cœurs.
Calculs massifs
- Somme, recherche du maximum/minimum, calcul de statistiques sur un grand tableau.
- Exemple : calculer la température moyenne sur un million de capteurs.
Traitement de collections
- Filtrage, tri, transformation de grandes listes (par exemple, traitement des commandes d’une boutique en ligne).
- Exemple : sélectionner toutes les commandes supérieures à 10 000 roubles et les trier par date.
Rendu et traitement graphique
- Appliquer un filtre à tous les pixels d’une image (par exemple la rendre en noir et blanc).
- Chaque pixel peut être traité indépendamment — cas idéal pour le parallélisme.
Analyse de données, big data
- MapReduce, agrégation, calcul de statistiques sur d’énormes volumes de données.
- Exemple : traitement des journaux d’une année pour détecter des anomalies.
Exemple : somme calculée en parallèle
Supposons que nous ayons un tableau d’1 million de nombres. On peut le diviser en 4 parties et calculer la somme de chaque partie dans un thread distinct, puis additionner les résultats.
4. Problèmes et défis du parallélisme
Complexité du débogage
Quand le code fonctionne sur plusieurs threads, les bugs peuvent n’apparaître que dans de rares cas, lorsque les threads « se croisent » d’une manière particulière. Parfois, l’erreur survient une fois sur 1000 exécutions — et il est très difficile de la capturer.
Conditions de course (race conditions)
Si plusieurs threads modifient simultanément la même variable ou le même objet — des résultats incorrects sont possibles. Par exemple, deux threads incrémentent un compteur en même temps et la valeur finale se révèle inférieure à celle attendue.
Synchronisation
Pour éviter les courses, il faut synchroniser l’accès aux données partagées — via le mot-clé synchronized, des verrous, des variables atomiques et d’autres outils. Cela complexifie le code et peut conduire à d’autres problèmes (par exemple, deadlock — interblocage mutuel des threads).
Équilibrage de charge
Si vous avez découpé la tâche en 4 parties, et que l’une d’elles s’avère bien plus lourde que les autres — trois threads ont déjà terminé et s’ennuient, tandis que le quatrième travaille toujours. Au final, pas d’accélération.
Surcoûts
Le lancement des threads, leur commutation, la synchronisation — tout cela prend du temps. Si la tâche est petite, le parallélisme ne fera que ralentir l’exécution.
Tableau : comparaison des approches
| Approche | Quand c’est rapide | Quand ça ralentit | Exemple d’utilisation |
|---|---|---|---|
| Séquentiel (1 thread) | Petites tâches, logique simple | Grands volumes de données | Traitement de 10 lignes |
| Multithreading (sur 1 cœur) | Tâches asynchrones (attente IO) | Tâches CPU-bound (limitées par les calculs processeur) sur 1 cœur | Téléchargement simultané de fichiers |
| Parallélisme (beaucoup de cœurs) | Grandes tâches indépendantes | Petites tâches, fort couplage | Traitement d’un grand tableau |
Visualisation : à quoi cela ressemble
// Traitement séquentiel (1 thread)
[Tâche 1][Tâche 2][Tâche 3][Tâche 4]
// Multithreading sur un seul cœur (logique de basculement)
[Tâche 1] [Tâche 2] [Tâche 3] [Tâche 4]
(mais en réalité une seule s’exécute à la fois, les autres attendent)
// Parallélisme sur quatre cœurs
[Tâche 1] [Tâche 2] [Tâche 3] [Tâche 4]
(toutes s’exécutent en même temps)
5. Erreurs typiques lors des tentatives de parallélisme
Erreur n° 1 : paralléliser tout et n’importe quoi. Beaucoup de débutants pensent : « Plus il y a de threads — plus c’est rapide ! ». En réalité, ce n’est pas le cas. Si les tâches sont peu nombreuses ou trop simples — le gain est absent, et parfois le programme fonctionne même plus lentement.
Erreur n° 2 : ignorer la synchronisation. Si plusieurs threads travaillent sur les mêmes données sans synchronisation — vous obtiendrez des conditions de course, une logique défaillante et des bugs difficiles à attraper.
Erreur n° 3 : le parallélisme pour le parallélisme. Le parallélisme n’est pas une fin en soi. Il est utile lorsqu’il existe de vraies tâches que l’on peut découper efficacement en parties indépendantes.
Erreur n° 4 : ne pas tenir compte des spécificités de la tâche. Certaines tâches ne peuvent tout simplement pas être parallélisées (par exemple lorsque l’étape N+1 dépend du résultat de l’étape N). Dans ces cas, le parallélisme n’apporte aucun avantage.
Erreur n° 5 : négliger les surcoûts. Le lancement des threads, leur commutation, la collecte des résultats — tout cela prend du temps. Pour les petites tâches, ce temps peut être supérieur au temps d’exécution lui-même.
GO TO FULL VERSION