Le traitement parallèle dans PowerShell — Choisir entre ForEach-Object -Parallel et les jobs
· Go Komura · PowerShell, Windows, Traitement parallèle, Amélioration des performances, Automatisation, Amélioration opérationnelle, Script, Job
« Le script qui ping 200 PC pour vérifier qu’ils sont en vie met 15 minutes à faire un tour complet. » « Le calcul de hachage de 100 000 fichiers sous un dossier partagé ne finit jamais. » Dès que l’automatisation PowerShell atteint une certaine maturité, on finit toujours par se heurter au mur du temps d’exécution. Et la plupart de ces traitements sont dominés par l’attente : le CPU est inactif, mais c’est lent parce qu’on attend, une par une et dans l’ordre, la réponse du réseau ou de l’E/S disque. C’est précisément là que la parallélisation entre en jeu.
Avec PowerShell 7, ForEach-Object -Parallel est devenu disponible, et la barrière à l’entrée du traitement parallèle a chuté de façon spectaculaire. Cela dit, il y a aussi un piège gênant : paralléliser sans précaution ne se contente pas de ne pas accélérer, cela ralentit, et en plus cela corrompt les résultats. « J’ai mis à jour une variable partagée et une valeur a disparu », « l’ordre de la sortie change à chaque fois », « la fonction définie par l’appelant est introuvable » sont autant de symptômes typiques d’une utilisation sans comprendre le mécanisme du traitement parallèle.
Cet article présente les différents moyens de parallélisation disponibles dans PowerShell, l’usage et les pièges de ForEach-Object -Parallel, comment choisir entre Start-ThreadJob et Start-Job, et jusqu’aux « cas où il ne faut pas paralléliser », sous une forme directement exploitable en pratique.
1. La conclusion, d’abord
ForEach-Object -Parallela été ajouté dans PowerShell 7.0. Il n’existe pas sous Windows PowerShell 5.1.1- Chaque bloc de script s’exécute dans une runspace (environnement d’exécution) différente. Les variables et fonctions de l’appelant ne sont pas visibles telles quelles. Les variables se transmettent avec le qualificateur de portée
$using:.1 - La valeur par défaut de
-ThrottleLimitest 5. Depuis PowerShell 7.1, le pool de runspaces est réutilisé, et ThrottleLimit devient la taille de ce pool. Pour en recréer une neuve à chaque fois, utilisez-UseNewRunspace.1 - Ce que transmet
$using:, c’est une « référence ». La lire seulement est sûr, mais pour la mettre à jour, il faut utiliser un type thread-safe deSystem.Collections.Concurrent. Une mise à jour concurrente d’une Hashtable ou d’une List ordinaire corrompt la structure.1 - La parallélisation n’est pas une mesure qui « accélère forcément ». La documentation officielle indique elle-même explicitement que paralléliser un traitement trivial peut être bien plus lent que la normale. Elle est efficace pour les traitements avec de longues attentes et pour les calculs qui ont vraiment du sens sur plusieurs cœurs.1
- L’ordre de la sortie comme des erreurs n’est pas garanti. L’ordre d’écriture dans le flux d’erreur est lui aussi aléatoire ; une erreur terminante à l’intérieur d’un bloc de script n’arrête que cette itération-là, et elle est signalée comme
PSTaskException.1 -AsJobpermet de le lancer comme un job. Mais-ThrottleLimitétant le degré de parallélisme par job, créer plusieurs jobs multiplie le nombre d’exécutions simultanées.1- Pour un parallélisme léger, Start-ThreadJob ; quand l’isolation est nécessaire, Start-Job. Start-Job s’exécute dans un processus séparé, ce qui entraîne une surcharge importante, et le résultat revient sérialisé, ayant perdu ses méthodes.23
- Pour envoyer le même traitement à plusieurs machines Windows, l’exécution parallèle implicite d’
Invoke-Commandest la voie la plus rapide. Elle exécute par défaut jusqu’à 32 machines simultanément.4
2. Les quatre options de parallélisation
2.1 Prérequis — qu’est-ce qu’une runspace ?
Fixons d’abord la notion de « runspace », qui revient constamment dans cet article. Une runspace est, à proprement parler, « l’environnement d’exécution » dans lequel PowerShell exécute un script. L’ensemble de l’état de la session — variables, fonctions, modules chargés, répertoire courant — appartient à cette runspace.
En termes de relation avec les processus et les threads, cela donne :
| Couche | Ce que c’est | Signification dans cet article |
|---|---|---|
| Processus | Unité d’exécution vue par l’OS (une instance de pwsh.exe) |
Un même processus peut contenir plusieurs runspaces |
| Thread | Le flux d’exécution qui fait réellement tourner le code | Une runspace s’exécute sur un thread |
| Runspace | Le conteneur de l’état PowerShell (variables, fonctions, modules) | Dès que cette couche est séparée, ni les variables ni les fonctions ne sont partagées |
L’image la plus proche est celle-ci : « on peut faire tenir, dans un seul processus, plusieurs sessions PowerShell possédant chacune leurs propres variables et fonctions. » ForEach-Object -Parallel est un mécanisme qui prépare plusieurs de ces runspaces et fait tourner chaque bloc de script sur un thread différent.1
Presque tous les tracas de cet article découlent de là. Les variables et fonctions de l’appelant appartiennent à sa propre runspace, et ne sont donc pas visibles depuis une autre runspace. C’est pourquoi les variables doivent être transmises explicitement avec $using:, et les fonctions doivent être modularisées et chargées dans chaque runspace. En revanche, comme le processus reste le même, les objets en mémoire peuvent, eux, finir par être partagés au niveau de leur instance réelle. Cela se retourne contre vous sous la forme des problèmes de sécurité des threads évoqués plus loin.
« Invisible, et pourtant partagé » — cette propriété en apparence contradictoire est la cause des accidents qui surviennent quand on parallélise sans comprendre les runspaces.
2.2 Les quatre moyens
Commençons par une carte d’ensemble. Il existe globalement quatre moyens de « faire tourner en même temps » dans PowerShell.
| Moyen | Unité d’exécution | Versions utilisables | Usage adapté |
|---|---|---|---|
ForEach-Object -Parallel |
Thread (runspace) | PowerShell 7.0+1 | Le même traitement sur chaque élément d’une collection. Premier choix |
Start-ThreadJob |
Thread (runspace) | Inclus dans PowerShell 7 / installable depuis la Gallery sous 5.12 | Lancer quelques traitements en arrière-plan et les récupérer plus tard |
Start-Job |
Processus séparé | Standard sous 5.1 comme sous 73 | Traitement nécessitant une isolation, ou que l’on veut faire tourner dans un processus distinct |
Invoke-Command -ComputerName |
Chaque PC distant | Standard sous 5.1 comme sous 74 | Distribuer le même traitement à plusieurs machines Windows |
En pratique, le choix est simple. Si vous appliquez le même traitement à un grand nombre de cibles, ForEach-Object -Parallel. Si les cibles sont « plusieurs PC distants », l’exécution distante passe en premier. Cette dernière exécute par défaut jusqu’à 32 ordinateurs simultanément parmi ceux que vous avez indiqués, sans que vous ayez à écrire vous-même de parallélisation.4 Pour la configuration de l’exécution distante elle-même, voir « Introduction à PowerShell Remoting (WinRM) ».
Start-Job est lourd parce que le job en arrière-plan est lancé comme un processus séparé. En plus du coût de démarrage du processus, le résultat revient sérialisé, si bien que l’objet reçu devient une « copie désérialisée » dépourvue de méthodes.3 Start-ThreadJob, à l’inverse, s’exécute sur un thread au sein du même processus, ce qui le rend nettement plus léger.2
3. Les bases de ForEach-Object -Parallel
Voici la forme minimale. $_ désigne l’objet d’entrée courant, et les variables externes se référencent en leur ajoutant $using:.1
$timeout = 2 # variable de l'appelant
$result = $computers | ForEach-Object -Parallel {
$name = $_
# une variable externe n'est visible qu'avec le préfixe $using:
$ok = Test-Connection -TargetName $name -Count 1 -TimeoutSeconds $using:timeout -Quiet
# la forme de base consiste à « produire une sortie » plutôt qu'à mettre à jour une variable partagée pour l'agrégation
[pscustomobject]@{
Computer = $name
Alive = $ok
CheckedAt = Get-Date
}
} -ThrottleLimit 20
$result | Where-Object { -not $_.Alive } | Format-Table
Il y a un principe de conception essentiel à retenir ici. Plutôt que de mettre à jour une variable partagée, chaque bloc de script « produit » son résultat en sortie, et l’appelant les récupère tous ensemble. En adoptant cette forme, les problèmes de sécurité des threads ne se posent tout simplement pas. Le code qui pose problème en traitement parallèle essaie presque toujours de modifier un état partagé.
Si vous devez absolument les rassembler dans une collection partagée, utilisez un type thread-safe.1
# Le schéma donné en exemple par la documentation officielle : ConcurrentDictionary peut être mis à jour en toute sécurité depuis plusieurs threads
$safeDict = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
Get-Process | ForEach-Object -Parallel {
$dict = $using:safeDict
# TryAdd() renvoie un booléen. Sans le jeter, True/False se mélange à la sortie
$null = $dict.TryAdd($_.ProcessName, $_)
}
# [DANGEREUX] transmettre une Hashtable ou une List<T> ordinaire avec $using: pour la mettre à jour n'est pas sûr
# $shared = @{} # une mise à jour concurrente corrompt la structure interne, provoquant des exceptions ou des pertes de valeurs
Précisons ici exactement le sens de $using:. La documentation officielle indique explicitement, à propos de ForEach-Object -Parallel, que « le qualificateur Using: transmet une référence de variable depuis le thread qui a appelé l’applet de commande vers le thread de chacun des blocs de script en cours d’exécution ». Ce qui est transmis, c’est une référence vers le même objet, au sein du même processus. C’est précisément pour cela que se contenter de lire quelque chose d’immuable est sûr, tandis que modifier un état nécessite un type thread-safe.1
Et cette propriété de « transmission par référence » est propre à la parallélisation qui s’exécute au sein du même processus. Start-Job s’exécute dans un processus séparé, Invoke-Command -ComputerName sur une autre machine, si bien que la valeur transmise avec $using: arrive sérialisée, sous forme de copie. La modifier de l’autre côté ne se répercute pas sur l’appelant, et l’objet qui revient est lui aussi une copie désérialisée dépourvue de méthodes.3 La syntaxe $using: est identique, mais son sens diffère : n’apportez pas dans Start-Job les réflexes acquis avec ForEach-Object -Parallel.
4. Cinq pièges
(1) Les fonctions de l’appelant ne sont pas visibles
Un bloc de script parallèle s’exécutant dans une runspace différente, les fonctions définies par l’appelant ne peuvent pas être appelées telles quelles. La bonne pratique consiste à modulariser le traitement commun et à l’importer en tête du bloc de script.
$items | ForEach-Object -Parallel {
Import-Module 'D:\Scripts\Modules\KsOps' -ErrorAction Stop # chargé dans chaque runspace
Convert-KsRecord -Input $_
} -ThrottleLimit 8
Cela dit, le coût du chargement du module dans chaque runspace se multiplie par le degré de parallélisme. Pour un traitement utilisant un module lourd, cela peut devenir la cause principale d’un plafonnement même en augmentant le degré de parallélisme. Les bonnes pratiques de modularisation sont résumées dans « Conception des arguments et modularisation dans PowerShell ».
(2) La réutilisation des runspaces peut faire persister un état
Sous PowerShell 7.0, une nouvelle runspace était créée à chaque itération, mais depuis 7.1, elle est par défaut réutilisée depuis un pool de runspaces.1 C’est une amélioration importante côté performance, mais cela signifie qu’une variable d’environnement définie, un changement de répertoire courant, ou l’état d’un module chargé lors d’une itération reste visible depuis les itérations suivantes qui réutilisent la même runspace. Si une indépendance complète entre les itérations est nécessaire, précisez -UseNewRunspace (ce qui ralentit d’autant).1
(3) Ni la sortie ni les erreurs n’ont un ordre garanti
L’ordre de l’exécution parallèle n’est pas déterministe. L’ordre d’écriture dans le flux d’erreur est lui aussi aléatoire, tout comme pour les flux d’avertissement, de détail et d’information.1
1..5 | ForEach-Object -Parallel {
if ($_ -eq 3) { throw "Terminating Error: $_" } # seule cette itération s'arrête
"Output: $_"
}
# Output: 1 / Output: 4 / Output: 2 / Output: 5 (ordre variable), Output: 3 n'apparaît pas
Une erreur terminante à l’intérieur d’un bloc de script ne met fin qu’à cette itération, les autres exécutions parallèles continuant. L’erreur est écrite dans le flux d’erreur comme un ErrorRecord dont le FullyQualifiedErrorId vaut PSTaskException. Le comportement n’est pas « un seul échec arrête tout », vous devez donc agréger vous-même le succès ou l’échec de chaque élément.1
$results = $files | ForEach-Object -Parallel {
# à l'intérieur d'un bloc catch, $_ devient un ErrorRecord,
# donc l'objet d'entrée doit toujours être sauvegardé dans une autre variable avant usage
$file = $_
try {
$hash = Get-FileHash -Path $file.FullName -Algorithm SHA256 -ErrorAction Stop
[pscustomobject]@{ Path = $file.FullName; Hash = $hash.Hash; Error = $null }
}
catch {
# renvoyer aussi l'échec comme « sortie », et le trier du côté de l'appelant
[pscustomobject]@{ Path = $file.FullName; Hash = $null; Error = $_.Exception.Message }
}
} -ThrottleLimit 8
$failed = $results | Where-Object Error
if ($failed) { Write-Warning "$($failed.Count) élément(s) en échec" }
(4) PipelineVariable n’est pas utilisable
Le paramètre commun -PipelineVariable n’est pas pris en charge dans les scénarios parallèles, même en le préfixant de $using:.1 C’est un point qui pose problème lors de la migration depuis un traitement séquentiel.
(5) L’affichage de progression et les journaux se mélangent
Écrire Write-Progress ou une journalisation maison simultanément depuis plusieurs threads mélange les lignes ou fait entrer les fichiers en conflit. Il est plus sûr de ne pas ajouter directement au fichier journal depuis l’intérieur du bloc de script, mais de le renvoyer comme un objet de résultat et de l’écrire depuis un seul endroit, chez l’appelant. La conception des flux de sortie est traitée dans « Flux de sortie et conception de la journalisation dans PowerShell ».
5. Comment déterminer ThrottleLimit
-ThrottleLimit désigne le nombre de blocs de script exécutés simultanément ; sa valeur par défaut est 5.1 Depuis 7.1, cette valeur devient directement la taille du pool de runspaces.1 Voici son fonctionnement représenté en schéma.
flowchart LR
IN["Entrée : 200 éléments"] --> Q["File d'attente<br/>attend qu'une place se libère"]
Q --> POOL
subgraph POOL["Pool de runspaces ThrottleLimit = 5"]
R1["Runspace 1"]
R2["Runspace 2"]
R3["Runspace 3"]
R4["Runspace 4"]
R5["Runspace 5"]
end
POOL --> OUT["Sortie<br/>ordre non garanti"]
POOL -.->|"à chaque élément terminé,<br/>réutilise la même runspace"| Q
Le point essentiel est que ce ne sont pas 200 runspaces qui sont créées pour 200 éléments d’entrée : seules autant de runspaces que le nombre indiqué par -ThrottleLimit sont préparées et réutilisées. Deux conséquences en découlent. D’une part, augmenter -ThrottleLimit augmente le coût en mémoire et en initialisation. D’autre part, cette réutilisation peut faire persister l’état d’une itération précédente (chapitre 4, point (2)).
Les repères pour choisir se répartissent selon la nature du traitement.
| Nature du traitement | Repère | Raison |
|---|---|---|
| Attente réseau (test de connectivité, appel d’API) | Peut dépasser le nombre de cœurs (essayer autour de 20 à 50) | Le CPU est quasiment inactif. La contrainte vient de l’autre côté |
| Attente d’E/S disque | Essayer autour de 8 à 16 | Trop augmenter accroît les accès aléatoires et devient contre-productif. La différence entre SSD et HDD est importante |
| Calcul CPU (hachage, compression, conversion) | Environ le nombre de cœurs logiques | Au-delà, ce n’est que du gaspillage en changements de contexte |
| Interlocuteur : serveur métier ou API | La capacité de l’autre partie est la limite | Dépasser le taux de limitation ou le plafond de connexions simultanées fait de vous la cause de l’incident |
La dernière ligne est la plus importante en pratique. Faire tomber un serveur métier pour accélérer son propre script serait complètement contre-productif, donc quand vous ciblez une API interne ou un serveur de fichiers, fixez votre degré de parallélisme « dans la limite de ce que l’autre partie peut supporter ». Pour la gestion du taux de limitation d’une API, voir « S’intégrer à une API REST avec PowerShell ».
La documentation officielle précise aussi une mise en garde pour l’utilisation de -AsJob : ThrottleLimit est une limite par appel de ForEach-Object -Parallel, donc créer 10 jobs fait tourner « 10 × ThrottleLimit » simultanément.1
Encore un point : même sous le nom identique -ThrottleLimit, ce qui est compté diffère selon la commande. Les confondre fausse l’estimation du degré de parallélisme, alignons donc les deux qui apparaissent dans cet article.
| Commande | Ce que compte -ThrottleLimit |
Valeur par défaut |
|---|---|---|
ForEach-Object -Parallel |
Le nombre de blocs de script exécutés simultanément (= la taille du pool de runspaces) | 51 |
Invoke-Command -ComputerName |
Le nombre d’ordinateurs connectés simultanément | 324 |
Autrement dit, Invoke-Command -ThrottleLimit 32 signifie « se connecter à 32 machines simultanément », et non « traiter avec un parallélisme de 32 par machine ». Si vous combinez les deux (faire tourner ForEach-Object -Parallel du côté distant), gardez à l’esprit que le nombre de machines × le degré de parallélisme devient la charge totale imposée à l’autre partie.
6. Les cas où ne pas paralléliser est plus rapide
La documentation officielle va jusqu’à écrire ceci sans détour : une nouvelle runspace entraîne une surcharge considérable par rapport au traitement séquentiel, et un script parallèle traitant une tâche triviale peut être bien plus lent que la normale — il faut essayer en pratique et trouver les endroits où cela apporte un gain.1 Il existe même, dans les exemples officiels, un échantillon explicitement annoté comme « ceci est un exemple inefficace d’utilisation du parallélisme ».
Les critères de décision sont simples.
- Le temps de traitement par élément est court (de l’ordre de la milliseconde) → ne pas paralléliser. Le traitement de chaînes, la consultation d’une Hashtable et autres opérations similaires sont plus rapides en séquentiel
- Le nombre d’éléments est faible (quelques dizaines) → ne pas paralléliser. La surcharge est relativement importante
- Chaque élément comporte une attente de plusieurs centaines de millisecondes ou plus → la parallélisation a de la valeur
- Il y a un calcul qui utilise le CPU pendant longtemps → efficace sur plusieurs cœurs
Et il faut absolument mesurer avant de décider. Comparer les versions séquentielle et parallèle avec Measure-Command donne la réponse en quelques dizaines de secondes. Les bonnes pratiques de mesure et l’accélération des scripts PowerShell en général sont résumées dans « Où regarder quand un script PowerShell est lent ».
# Comparer le séquentiel et le parallèle sur la même entrée (exécuter plusieurs fois pour observer une valeur stable)
# Le côté parallèle s'exécute dans une runspace différente, donc les fonctions définies par l'appelant ne sont pas visibles.
# Comme la comparaison ne serait pas valide sinon, chargez le module ou écrivez le traitement directement
$seq = Measure-Command {
$items | ForEach-Object { Invoke-Work $_ }
}
$par = Measure-Command {
$items | ForEach-Object -Parallel {
Import-Module 'D:\Scripts\Modules\KsOps' # ← sans cela, la commande n'est pas trouvée
Invoke-Work $_
} -ThrottleLimit 16
}
'{0:N1} s → {1:N1} s' -f $seq.TotalSeconds, $par.TotalSeconds
7. Choisir entre Start-ThreadJob et Start-Job
ForEach-Object -Parallel convient bien à la forme « appliquer le même traitement à de nombreuses entrées », mais quand vous voulez faire tourner simultanément des traitements de nature différente et les récupérer plus tard, les jobs sont plus adaptés.
# Faire tourner des traitements séparés simultanément, puis attendre tous ensemble (job de thread = léger)
# Un job de thread s'exécute lui aussi dans une runspace différente, donc les fonctions de l'appelant ne sont pas visibles.
# Charger le module en tête de chaque job (ou utiliser un bloc de script autonome)
$jobs = @(
Start-ThreadJob -Name 'Inventaire des utilisateurs AD' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsAdUserReport
}
Start-ThreadJob -Name 'Capacité du serveur de fichiers' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsShareUsage
}
Start-ThreadJob -Name 'Récapitulatif des licences' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsLicenseSummary
}
)
# Attendre la fin et récupérer les résultats. Toujours capturer les erreurs avec -ErrorVariable
$reports = $jobs | Receive-Job -Wait -ErrorAction SilentlyContinue -ErrorVariable jobErrors
# L'état ne passe à Failed que lorsque le bloc de script s'est arrêté sur une « erreur terminante ».
# Un job qui s'est terminé sur une erreur non terminante comme Write-Error reste à Completed,
# donc se fier uniquement à l'état fait passer « une erreur est survenue » pour un succès
$failed = @($jobs | Where-Object { $_.State -eq 'Failed' })
$jobs | Remove-Job -Force
if ($failed.Count -gt 0 -or @($jobErrors).Count -gt 0) {
throw "Une erreur est survenue pendant l'exécution parallèle : $(($jobErrors | ForEach-Object { $_.Exception.Message }) -join ' / ')"
}
Ne pas utiliser -AutoRemoveJob ici est intentionnel : le job disparaîtrait dès sa récupération, empêchant de vérifier lequel a échoué. Les erreurs de Receive-Job sont par défaut des erreurs non terminantes ; si vous n’écrivez rien pour les gérer, vous obtenez la situation la plus gênante qui soit : « une partie a échoué, mais seule la partie réussie revient, en donnant l’impression que tout s’est terminé ». Dans l’automatisation des inventaires ou des rapports, cette perte silencieuse subsiste sous forme d’erreurs de chiffres.
Choisir Start-Job (isolation par processus) se justifie dans les cas suivants.3
- Un traitement que vous ne voulez pas voir entraîner le processus appelant (par exemple, appeler une DLL native susceptible de planter)
- Un traitement nécessitant un environnement de processus différent (vouloir l’exécuter avec des paramètres de culture ou des variables d’environnement différents)
- Un traitement où vous voulez séparer l’environnement d’exécution lui-même, comme 32 bits / 64 bits
À l’inverse, si aucun de ces cas ne s’applique, Start-Job est désavantagé, ne serait-ce qu’à cause de la surcharge et des contraintes de sérialisation. Le fait que l’objet désérialisé n’ait pas de méthodes se répercute aussi sur le traitement en aval.3
8. Dans un environnement limité à Windows PowerShell 5.1
-Parallel n’est pas utilisable sous 5.1. Il existe trois options réalistes.
Start-ThreadJob(installer le moduleThreadJobdepuis PowerShell Gallery) — permet un parallélisme léger même sous 5.1. Pour les bonnes pratiques de distribution interne, voir « Distribuer et mettre à jour des modules PowerShell en interne »2Invoke-Command -ComputerName— si les cibles sont plusieurs machines Windows, cela suffit à obtenir du parallélisme (32 machines simultanées par défaut)4- Installer PowerShell 7 — 5.1 et 7 pouvant coexister, ne faire tourner sous 7 que les traitements lourds est aussi une option réaliste
L’installation de la première option se fait ainsi. Exécuter simplement Install-Module sur un 5.1 vierge peut échouer, aussi les deux points sur lesquels on trébuche facilement sont indiqués en même temps.
# [1] PowerShell Gallery exige TLS 1.2 ou une version ultérieure.
# Ce n'est pas activé par défaut sous Windows PowerShell 5.1,
# ce qui peut provoquer un échec avec un message du type « la connexion sous-jacente a été fermée ».
# Activez TLS 1.2 pour cette session seulement, avant d'exécuter la suite
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# [2] Installation. Avec CurrentUser, les droits administrateur ne sont pas nécessaires
Install-Module ThreadJob -Scope CurrentUser
# [3] Vérification
Import-Module ThreadJob
Get-Command -Module ThreadJob # si Start-ThreadJob apparaît, l'installation a réussi
Les deux points sur lesquels on trébuche facilement sont les suivants.
- TLS 1.2 — depuis avril 2020, PowerShell Gallery suppose une connexion en TLS 1.2 ou une version ultérieure. Sous 5.1, la ligne ci-dessus peut être nécessaire5
- Confiance du dépôt et politique d’exécution — PSGallery est par défaut traité comme un « dépôt non approuvé », ce qui fait que
Install-Moduledemande une confirmation. Pour une installation sans surveillance, utilisez-Force; pour un usage régulier, marquez-le une fois pour toutes comme approuvé avecSet-PSRepository -Name PSGallery -InstallationPolicy Trusted. De plus, si la politique d’exécution estRestricted(la valeur par défaut pour un client Windows), le chargement du script s’arrête, il faut donc un réglage équivalent àSet-ExecutionPolicy -Scope CurrentUser RemoteSigned. Évitez de passer en permanence àUnrestricted6
La troisième option finit souvent par être la plus économique dans l’ensemble, et nous recommandons nous aussi de « faire tourner ce seul traitement sous 7 » plutôt que « d’écrire un code 5.1 sophistiqué pour paralléliser ». La philosophie de la coexistence et de la migration est résumée dans « Les différences entre Windows PowerShell 5.1 et PowerShell 7 ».
9. Les bonnes pratiques (tableau de décision)
| Ce que vous voulez faire | Choix | Remarque |
|---|---|---|
| Le même traitement sur de nombreuses cibles (test de connectivité, calcul de hachage, appel d’API) | ForEach-Object -Parallel |
Réservé à PowerShell 7. Concevez pour renvoyer le résultat en sortie1 |
| Le même traitement sur plusieurs machines Windows | Invoke-Command -ComputerName |
32 machines simultanées par défaut. Pas besoin de coder votre propre parallélisation4 |
| Exécuter simultanément des traitements de nature différente | Start-ThreadJob |
Léger. Récupération avec Receive-Job -Wait2 |
| Isolation de processus nécessaire | Start-Job |
Lourd. Le résultat est désérialisé3 |
| Vouloir rassembler les résultats en un seul endroit | Renvoyer en sortie / ConcurrentDictionary |
La mise à jour concurrente d’une Hashtable ou d’une List ordinaire est impossible1 |
| Chaque élément est léger / peu d’éléments | Ne pas paralléliser | La surcharge ralentit au lieu d’accélérer1 |
| Interlocuteur : serveur métier ou API | Fixer ThrottleLimit selon la capacité de l’autre partie | Le taux de limitation et le plafond de connexions simultanées constituent la limite de fait |
| Indépendance entre itérations indispensable | -UseNewRunspace |
Évite la persistance d’état due à la réutilisation (ralentit)1 |
10. Conclusion
ForEach-Object -Parallelest une fonctionnalité de PowerShell 7.0 et versions ultérieures, qui exécute chaque bloc de script dans une runspace différente. La forme de base consiste à transmettre les variables de l’appelant avec$using:et à charger les fonctions sous forme de module.- Plutôt que de mettre à jour une variable partagée, une conception où chaque itération produit son résultat en sortie et l’appelant les agrège permet d’éviter presque tous les problèmes de sécurité des threads. S’il faut absolument partager, utilisez un type de la famille Concurrent.
- Le ThrottleLimit par défaut est 5. Pour un traitement dominé par l’attente, une valeur élevée convient ; pour un calcul CPU, environ le nombre de cœurs ; pour un traitement avec un interlocuteur, sa propre capacité constitue la limite.
- L’ordre de la sortie et des erreurs n’est pas garanti, et une erreur terminante n’arrête que cette itération. Agrégez vous-même le succès ou l’échec de chaque élément.
- La parallélisation n’est pas une solution universelle. Pour un traitement léger ou peu d’éléments, la surcharge ralentit. Comparez toujours avec la version séquentielle via
Measure-Commandavant de l’adopter. - Dans un environnement 5.1,
Start-ThreadJob; à distance,Invoke-Command; et si cela ne suffit toujours pas, envisager l’installation de PowerShell 7 est l’approche la plus réaliste.
Téléchargement du code d’exemple
Le code traité dans cet article est distribué sous une forme prête à l’emploi. Il comprend des modèles pratiques pour ForEach-Object -Parallel et Start-ThreadJob.
Télécharger le code d’exemple (zip)
Les exemples de cet article ont été effectivement exécutés et vérifiés sous PowerShell 7.6 (8 tests Pester). En exécutant Invoke-SampleTests.ps1 inclus dans le zip, vous pouvez reproduire la même vérification chez vous.
# Analyse syntaxique + analyse statique + tests Pester
./Invoke-SampleTests.ps1
Les valeurs de configuration (chemins, noms de serveurs, ID de locataire, etc.) sont données à titre d’exemple. Ne les exécutez pas telles quelles en production : adaptez-les à votre propre environnement.
Articles connexes
- Les différences entre Windows PowerShell 5.1 et PowerShell 7 — guide pratique de migration des scripts internes
- Introduction à PowerShell Remoting (WinRM) — gérer plusieurs machines Windows en une seule fois
- Conception des arguments et modularisation des scripts PowerShell — d’un « script qui fonctionne » à un « script que l’on peut transmettre »
- Faire l’inventaire d’un serveur de fichiers avec PowerShell — audit de la capacité et des droits d’accès (ACL)
- Gestion des erreurs et conception des relances dans PowerShell — des pièges où try/catch ne fonctionne pas jusqu’aux bonnes pratiques d’exit code et de retry
Domaines de conseil associés
合同会社小村ソフト (Komura Software LLC) prend en charge l’accélération des scripts d’exploitation chronophages, la revue de conception d’automatisations incluant du traitement parallèle, et l’investigation d’anomalies du type « les résultats sont devenus incohérents après la parallélisation ».
- Conseil technique et revue de conception
- Investigation des anomalies et analyse des causes
- Migration et valorisation des actifs existants
- Contact
Références
-
Microsoft Learn, ForEach-Object. Sur l’ajout du jeu de paramètres -Parallel dans PowerShell 7.0, sur le fait que chaque bloc de script s’exécute dans une nouvelle runspace, sur la transmission de variables via le qualificateur de portée $using:, sur le fait que la valeur par défaut de -ThrottleLimit est 5 et devient la taille du pool de runspaces, sur le fait que depuis 7.1 les runspaces sont réutilisées et peuvent être recréées avec -UseNewRunspace, sur le comportement de -AsJob et -TimeoutSeconds, sur le fait que la mise à jour d’une référence transmise avec $using: nécessite un type thread-safe comme ceux de System.Collections.Concurrent, sur le fait que l’ordre de sortie des erreurs non terminantes n’est pas garanti, sur le fait qu’une erreur terminante ne met fin qu’à l’instance parallèle concernée et que son FullyQualifiedErrorId devient PSTaskException, sur le fait que PipelineVariable n’est pas pris en charge dans les scénarios parallèles, sur le fait que la surcharge d’une nouvelle runspace est importante et peut ralentir un traitement trivial, et sur le fait que ThrottleLimit est une limite par job lors de l’utilisation de -AsJob. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25 ↩26
-
Microsoft Learn, Start-ThreadJob. Sur le fait que Start-ThreadJob exécute le bloc de script sur un thread indépendant au sein du même processus plutôt que dans un processus séparé, ce qui le rend plus léger que Start-Job, sur le fait qu’il peut être manipulé avec les applets de commande de job standard (Receive-Job, etc.), et sur le contrôle du nombre d’exécutions simultanées via -ThrottleLimit. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Jobs. Sur le fait qu’un job en arrière-plan exécute une commande de façon asynchrone dans un nouveau processus, sur la manipulation des jobs via Start-Job, Get-Job, Receive-Job et Wait-Job, et sur le fait que le résultat d’un job revient après être passé par une sérialisation. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Invoke-Command. Sur la possibilité d’indiquer plusieurs ordinateurs avec -ComputerName pour exécuter la même commande, sur le fait que -ThrottleLimit limite le nombre de connexions simultanées avec une valeur par défaut de 32, et sur l’exécution en arrière-plan via -AsJob. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft PowerShell Team Blog, PowerShell Gallery TLS Support. Sur le fait que depuis avril 2020, PowerShell Gallery utilise TLS 1.2 par défaut et exige une connexion cliente en TLS 1.2 ou une version ultérieure, et sur la solution de contournement conseillée sous Windows PowerShell 5.1, consistant à définir Tls12 sur
[Net.ServicePointManager]::SecurityProtocolavant de se connecter. ↩ -
Microsoft Learn, about_Execution_Policies. Sur le fait que la politique d’exécution est le mécanisme qui contrôle les conditions de chargement des scripts, sur le fait que la politique effective quand elle n’est définie dans aucune portée est Restricted pour un client Windows et RemoteSigned pour Windows Server, sur le fait que Restricted empêche l’exécution des fichiers de script tels que .ps1 et .psm1, et sur la possibilité d’utiliser -Scope pour ne l’appliquer qu’à l’utilisateur courant ou au processus courant. ↩
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Où regarder quand un script PowerShell est lent — les points clés sur les tableaux, le pipeline et le rapprochement de données
Ce guide passe en revue les causes classiques de lenteur des scripts PowerShell : pourquoi l'opérateur += sur un tableau devient O(n²), l...
Arrêter d'utiliser Write-Host — Conception des flux de sortie et de la journalisation dans PowerShell
Ce guide présente les six flux de sortie de PowerShell et leurs usages respectifs, les problèmes posés par Write-Host et son bon usage, l...
Gérer les informations d'identification en toute sécurité sous PowerShell — bannir les mots de passe en clair de vos scripts
Un guide pratique pour faire migrer les mots de passe en clair d'un script PowerShell vers un stockage sécurisé : la réalité et les limit...
Conception des paramètres et modularisation des scripts PowerShell — Du « script qui fonctionne » au « script que l'on peut transmettre »
Ce guide organise les étapes pour élever la qualité d'un script PowerShell jusqu'à pouvoir le transmettre à d'autres personnes. Il couvre...
Gestion des erreurs et conception des nouvelles tentatives sous PowerShell — du piège du try/catch aux bonnes pratiques d'exit code et de retry
Cet article présente, du point de vue pratique, la différence entre erreurs terminales et non terminales sous PowerShell, le piège du try...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Applications métier, intégration d'équipements et outils de communication, des besoins au développement.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- ForEach-Object -Parallel est-il utilisable sous Windows PowerShell 5.1 ?
- Non. -Parallel est un jeu de paramètres ajouté dans PowerShell 7.0, et il n'existe tout simplement pas sous 5.1. Pour paralléliser sous 5.1, les options sont Start-ThreadJob du module ThreadJob (installable depuis PowerShell Gallery), Start-Job standard (isolé par processus, donc lourd), ou construire soi-même un RunspacePool. Si l'objectif est d'accélérer des scripts internes, il est aussi plus avantageux en termes de maintenabilité d'installer PowerShell 7 et d'utiliser -Parallel plutôt que d'écrire un traitement parallèle sophistiqué sous 5.1.
- J'ai parallélisé, mais c'est devenu plus lent. Pourquoi ?
- Parce que la surcharge de l'exécution parallèle dépasse le traitement lui-même. ForEach-Object -Parallel exécute chaque bloc de script dans une runspace séparée, ce qui entraîne une surcharge considérable par rapport au traitement séquentiel ; la documentation officielle précise d'ailleurs explicitement qu'un « script parallèle traitant une tâche triviale peut être bien plus lent que la normale ». Quand un traitement individuel se termine en quelques millisecondes, ou quand il n'y a que quelques dizaines d'éléments, il est normal que ne pas paralléliser soit plus rapide. L'effet se fait sentir sur les traitements avec de longues attentes réseau ou d'E/S fichier, ou sur des calculs qui ont vraiment du sens sur plusieurs cœurs.
- Peut-on écrire une valeur, depuis l'intérieur d'un bloc de script parallèle, dans une variable transmise avec $using: ?
- Lire la valeur référencée est sûr, mais l'écrire ne l'est pas, sauf si le type cible est thread-safe. $using: est un mécanisme qui transmet une référence de variable depuis le thread appelant vers le thread de chaque bloc de script ; comme plusieurs threads y touchent simultanément, mettre à jour une Hashtable ou une List ordinaire finit par corrompre la structure. Si vous voulez collecter des résultats agrégés, utilisez un type thread-safe comme ConcurrentDictionary ou ConcurrentBag de l'espace de noms System.Collections.Concurrent, ou concevez plutôt chaque bloc de script pour qu'il produise une valeur en sortie, récupérée ensuite par l'appelant.
- Quelle est la bonne valeur pour ThrottleLimit ?
- Cela dépend de la nature du traitement. La valeur par défaut est 5. Pour un traitement dominé par l'attente réseau ou l'E/S fichier (test de connectivité à un serveur, appel d'API, etc.), une valeur supérieure au nombre de cœurs CPU reste efficace. En revanche, pour un calcul qui sature le CPU, dépasser le nombre de cœurs ne fait que ralentir à cause de la contention. Quand l'interlocuteur est un serveur métier ou une API, la limite ne dépend pas que de vous : la limite de connexions simultanées ou le taux de limitation côté serveur compte aussi. La démarche sûre consiste à mesurer d'abord avec la valeur par défaut de 5, puis à doubler pour voir si cela apporte un gain.
- Je n'arrive pas à appeler mes propres fonctions depuis l'intérieur d'un bloc de script parallèle.
- Un bloc de script parallèle s'exécute dans une runspace différente de celle de l'appelant, si bien que les fonctions et variables définies dans la portée de l'appelant ne sont pas visibles telles quelles. Il y a deux solutions : transformer le traitement commun en module (.psm1) et faire un Import-Module en tête du bloc de script, ou transmettre la définition de la fonction comme texte via $using: et la redéfinir à l'intérieur du bloc de script. Du point de vue de la maintenabilité, la première option est recommandée. Attention cependant : charger le module a un coût dans chaque runspace, donc si le module est lourd, augmenter le degré de parallélisme finit par plafonner.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.