PowerShell Multithreading: Un plongeon profond

À un moment donné, la plupart des gens se heurteront à un problème pour lequel un script PowerShell de base est tout simplement trop lent pour résoudre. Cela peut consister à collecter des données à partir de nombreux ordinateurs de votre réseau ou peut-être à créer un grand nombre de nouveaux utilisateurs dans Active Directory en une seule fois. Ce sont tous de bons exemples où l’utilisation de plus de puissance de traitement permettrait à votre code de s’exécuter plus rapidement. Voyons comment résoudre cela en utilisant le multithreading PowerShell !

La session PowerShell par défaut est mono-thread. Elle exécute une commande et, une fois terminée, passe à la commande suivante. C’est bien car cela permet de tout répéter et n’utilise pas beaucoup de ressources. Mais que se passe-t-il si les actions qu’elle effectue ne dépendent pas les unes des autres et que vous avez des ressources CPU à revendre ? Dans ce cas, il est temps de commencer à réfléchir au multithreading.

Dans cet article, vous allez apprendre comment comprendre et utiliser différentes techniques de multithreading PowerShell pour traiter plusieurs flux de données en même temps, mais gérés par la même console.

Comprendre le multithreading PowerShell

Le multithreading est un moyen d’exécuter plusieurs commandes en même temps. Alors que PowerShell utilise normalement un seul thread, il existe de nombreuses façons d’en utiliser plusieurs pour paralléliser votre code.

L’avantage principal du multithreading est de réduire le temps d’exécution du code. Cette réduction du temps se fait au détriment d’une exigence de puissance de traitement plus élevée. Lors du multithreading, de nombreuses actions sont effectuées en même temps, ce qui nécessite donc plus de ressources système.

Par exemple, que se passerait-il si vous vouliez créer un nouvel utilisateur dans Active Directory ? Dans cet exemple, il n’y a rien à exécuter en parallèle car une seule commande est exécutée. Tout cela change lorsque vous souhaitez créer 1000 nouveaux utilisateurs.

Sans l’exécution en parallèle, vous exécuteriez la commande New-ADUser 1000 fois pour créer tous les utilisateurs. Peut-être que cela prend trois secondes pour créer un nouvel utilisateur. Pour créer les 1000 utilisateurs, cela prendrait un peu moins d’une heure. Au lieu d’utiliser un seul thread pour 1000 commandes, vous pourriez utiliser 100 threads exécutant chacun dix commandes. Maintenant, au lieu de prendre environ 50 minutes, vous êtes passé à moins d’une minute !

Notez que vous n’obtiendrez pas une mise à l’échelle parfaite. Le fait de créer et de supprimer des éléments dans le code prendra du temps. Avec un seul thread, PowerShell doit exécuter le code et c’est terminé. Avec plusieurs threads, le thread d’origine utilisé pour exécuter votre console sera utilisé pour gérer les autres threads. À un certain point, ce thread d’origine sera saturé simplement en maintenant tous les autres threads en ligne.

Prérequis pour l’exécution en parallèle de PowerShell

Dans cet article, vous allez apprendre comment fonctionne l’exécution en parallèle de PowerShell de manière pratique. Si vous souhaitez suivre, voici quelques éléments dont vous aurez besoin et quelques détails sur l’environnement qui est utilisé.

  • Windows PowerShell version 3 ou supérieure – Tout, sauf indication contraire, le code présenté fonctionnera dans Windows PowerShell version 3 ou supérieure. La version 5.1 de Windows PowerShell sera utilisée pour les exemples.
  • CPU et mémoire supplémentaires – Vous aurez besoin d’au moins un peu de CPU et de mémoire supplémentaires pour paralléliser avec PowerShell. Si vous ne disposez pas de ces ressources, vous ne verrez peut-être aucun avantage en termes de performances.

Priorité n°1 : Corrigez votre code !

Avant de vous plonger dans l’accélération de vos scripts avec le multithreading PowerShell, il y a quelques préparatifs que vous voudrez effectuer. La première étape consiste à optimiser votre code.

Bien que vous puissiez attribuer davantage de ressources à votre code pour l’exécuter plus rapidement, le multithreading apporte une complexité supplémentaire. Si vous pouvez accélérer votre code avant d’utiliser le multithreading, vous devriez le faire en premier.

Identifiez les goulots d’étranglement.

L’une des premières étapes de la parallélisation de votre code consiste à déterminer ce qui le ralentit. Le code peut être lent en raison d’une mauvaise logique ou de boucles supplémentaires où vous pouvez apporter des modifications pour une exécution plus rapide avant d’utiliser le multithreading.

Un exemple courant pour accélérer votre code est de déplacer votre filtrage vers la gauche. Si vous interagissez avec un ensemble de données, tout filtrage que vous souhaitez effectuer pour réduire la quantité de données doit être effectué le plus tôt possible. Voici un exemple de code pour obtenir la quantité de CPU utilisée par le processus svchost.

L’exemple ci-dessous lit tous les processus en cours d’exécution, puis filtre un seul processus (svchost). Il sélectionne ensuite la propriété CPU et s’assure que la valeur n’est pas nulle.

PS51> Get-Process | Where-Object {$_.ProcessName -eq 'svchost'} | 
	Select-Object CPU | Where-Object {$_.CPU -ne $null}

Comparez le code ci-dessus à l’exemple ci-dessous. Voici un autre exemple de code qui produit le même résultat mais qui est organisé différemment. Remarquez que le code ci-dessous est plus simple et déplace toute la logique possible à gauche du symbole de pipe. Cela empêche Get-Process de renvoyer des processus qui ne vous intéressent pas.

PS51> Get-Process -Name 'svchost' | Where-Object {$_.CPU -ne $null} | 
	Select-Object CPU

Ci-dessous, la différence de temps d’exécution des deux lignes précédentes. Bien que la différence de 117 ms ne soit pas perceptible si vous exécutez ce code une seule fois, elle s’accumulera si vous l’exécutez des milliers de fois.

time difference for running the two lines from above

Utilisation d’un code résistant aux threads

Ensuite, assurez-vous que votre code est « résistant aux threads ». Le terme « résistant aux threads » signifie que si un thread exécute un code, un autre thread peut exécuter le même code en même temps sans causer de conflit.

Par exemple, écrire dans le même fichier à partir de deux threads différents n’est pas résistant aux threads car il ne saura pas quoi ajouter au fichier en premier. Tandis que deux threads lisant à partir d’un fichier sont résistants aux threads car le fichier n’est pas modifié. Les deux threads obtiennent la même sortie.

Le problème avec un code multithread PowerShell qui n’est pas résistant aux threads est que vous pouvez obtenir des résultats incohérents. Parfois, cela peut fonctionner correctement car les threads réussissent à synchroniser leur exécution pour éviter les conflits. D’autres fois, vous rencontrerez un conflit et le dépannage de l’erreur sera difficile en raison des erreurs incohérentes.

Si vous n’exécutez que deux ou trois tâches à la fois, il se peut qu’elles s’alignent correctement et écrivent toutes dans le fichier à des moments différents. Ensuite, lorsque vous augmentez le code à 20 ou 30 tâches, la probabilité que au moins deux des tâches essaient d’écrire en même temps diminue considérablement.

Exécution parallèle avec PSJobs

Une des façons les plus simples de créer des scripts multithread est d’utiliser PSJobs. Les PSJobs disposent de cmdlets intégrées dans le module Microsoft.PowerShell.Core. Le module Microsoft.PowerShell.Core est inclus dans toutes les versions de PowerShell depuis la version 3. Les commandes de ce module vous permettent d’exécuter du code en arrière-plan tout en continuant à exécuter un code différent en premier plan. Vous pouvez voir toutes les commandes disponibles ci-dessous.

PS51> Get-Command *-Job
Get-Command *-Job output

Suivi de vos tâches

Tous les PSJobs sont dans l’un des onze états. Ces états permettent à PowerShell de gérer les tâches.

Ci-dessous, vous trouverez une liste des états les plus courants dans lesquels une tâche peut se trouver.

  • Terminé – La tâche est terminée et les données de sortie peuvent être récupérées ou la tâche peut être supprimée.
  • En cours d’exécution – La tâche est en cours d’exécution et ne peut pas être supprimée sans arrêter de force la tâche. La sortie ne peut pas encore être récupérée.
  • Bloqué – La tâche est toujours en cours d’exécution, mais l’hôte est invité à fournir des informations avant de pouvoir continuer.
  • Échec – Une erreur terminale s’est produite pendant l’exécution de la tâche.

Pour obtenir l’état d’une tâche démarrée, vous utilisez la commande Get-Job. Cette commande récupère tous les attributs de vos tâches.

Voici la sortie pour une tâche où vous pouvez voir que l’état est Terminée. L’exemple ci-dessous exécute le code Start-Sleep 5 dans une tâche en utilisant la commande Start-Job. L’état de cette tâche est ensuite renvoyé en utilisant la commande Get-Job.

PS51> Start-Job -Scriptblock {Start-Sleep 5}
PS51> Get-Job
Get-Job output

Lorsque l’état de la tâche est Terminée, cela signifie que le code dans le bloc de script a été exécuté et a terminé son exécution. Vous pouvez également voir que la propriété HasMoreData est False. Cela signifie qu’il n’y avait aucune sortie à fournir après la fin de la tâche.

Voici un exemple de certains des autres états utilisés pour décrire les tâches. Vous pouvez voir dans la colonne Command ce qui a pu causer l’échec de certaines tâches, comme essayer de dormir pendant abc secondes.

All jobs

Création de nouvelles tâches

Comme vous l’avez vu précédemment, la commande Start-Job vous permet de créer une nouvelle tâche qui commence à exécuter du code. Lorsque vous créez une tâche, vous fournissez un bloc de script qui est utilisé pour la tâche. La PSJob crée alors une tâche avec un numéro d’ID unique et commence à exécuter la tâche.

Le principal avantage ici est que la commande Start-Job met moins de temps à s’exécuter que le scriptblock que nous utilisons. Vous pouvez voir dans l’image ci-dessous que plutôt que la commande prenant cinq secondes pour se terminer, elle a seulement pris 0,15 seconde pour démarrer le travail.

How long it takes to create a job

La raison pour laquelle elle a pu exécuter le même code en une fraction du temps est qu’il s’exécutait en arrière-plan en tant que PSJob. Il a fallu 0,15 seconde pour configurer et démarrer l’exécution du code en arrière-plan au lieu de l’exécuter en premier plan et de réellement attendre cinq secondes.

Récupération de la sortie du travail

Parfois, le code à l’intérieur du travail renvoie une sortie. Vous pouvez récupérer la sortie de ce code en utilisant la commande Receive-Job. La commande Receive-Job accepte un PSJob en tant qu’entrée, puis écrit la sortie du travail dans la console. Tout ce qui a été produit par le travail pendant son exécution a été stocké, de sorte que lorsque le travail est récupéré, il affiche tout ce qui a été stocké à ce moment-là.

Un exemple de cela serait d’exécuter le code ci-dessous. Cela créera et démarrera un travail qui écrira Hello World dans la sortie. Ensuite, il récupère la sortie du travail et l’affiche dans la console.

$Job = Start-Job -ScriptBlock {Write-Output 'Hello World'}
Receive-Job $Job
Receive-Job output

Création de travaux planifiés

Une autre façon d’interagir avec les PSJobs est d’utiliser une tâche planifiée. Les tâches planifiées sont similaires à une tâche planifiée Windows qui peut être configurée avec Task Scheduler. Les tâches planifiées permettent de planifier facilement des blocs de script PowerShell complexes dans une tâche planifiée. En utilisant une tâche planifiée, vous pouvez exécuter un PSJob en arrière-plan en fonction de déclencheurs.

Déclencheurs de tâche

Les déclencheurs de tâche peuvent être des choses comme une heure spécifique, lorsque l’utilisateur se connecte, lorsque le système démarre et bien d’autres. Vous pouvez également faire en sorte que les déclencheurs se répètent à des intervalles. Tous ces déclencheurs sont définis avec la commande New-JobTrigger. Cette commande est utilisée pour spécifier un déclencheur qui exécutera la tâche planifiée. Une tâche planifiée sans déclencheur doit être exécutée manuellement, mais chaque tâche peut avoir de nombreux déclencheurs.

En plus d’avoir un déclencheur, vous auriez toujours un bloc de script, tout comme avec un PSJob normal. Une fois que vous avez à la fois le déclencheur et le bloc de script, vous utilisez la commande Register-ScheduledJob pour créer la tâche, comme le montre la section suivante. Cette commande est utilisée pour spécifier les attributs de la tâche planifiée, tels que le bloc de script qui va être exécuté et les déclencheurs créés avec la commande New-JobTrigger.

Démonstration

Peut-être avez-vous besoin de faire tourner du code PowerShell à chaque fois que quelqu’un se connecte à un ordinateur. Vous pouvez créer une tâche planifiée à cet effet.

Pour ce faire, vous devez d’abord définir un déclencheur en utilisant New-JobTrigger et définir la tâche planifiée comme indiqué ci-dessous. Cette tâche planifiée écrira une ligne dans un fichier journal à chaque fois que quelqu’un se connecte.

$Trigger = New-JobTrigger -AtLogon
$Script = {"User $env:USERNAME logged in at $(Get-Date -Format 'y-M-d H:mm:ss')" | Out-File -FilePath C:\Temp\Login.log -Append}

Register-ScheduledJob -Name Log_Login -ScriptBlock $Script -Trigger $Trigger

Une fois que vous avez exécuté les commandes ci-dessus, vous obtiendrez une sortie similaire à celle de la création d’une nouvelle tâche qui affichera l’ID de la tâche, le scriptblock et d’autres attributs, comme indiqué ci-dessous.

Registering a scheduled job

Après quelques tentatives de connexion, vous pouvez voir sur la capture d’écran ci-dessous qu’elles ont été enregistrées.

Example login.log text file

Exploiter le paramètre AsJob

Une autre façon d’utiliser les tâches consiste à utiliser le paramètre AsJob, qui est intégré à de nombreuses commandes PowerShell. Étant donné qu’il existe de nombreuses commandes différentes, vous pouvez les trouver toutes en utilisant Get-Command, comme indiqué ci-dessous.

PS51> Get-Command -ParameterName AsJob

Une des commandes les plus courantes est Invoke-Command. Normalement, lorsque vous exécutez cette commande, elle commencera à exécuter une commande immédiatement. Alors que certaines commandes renverront immédiatement, vous permettant de continuer ce que vous faisiez, d’autres attendront que la commande soit terminée.

L’utilisation du paramètre AsJob fait exactement ce que son nom suggère et exécute la commande exécutée en tant que tâche au lieu de l’exécuter de manière synchrone dans la console.

Bien que la plupart du temps, AsJob puisse être utilisé avec la machine locale, Invoke-Command n’a pas d’option native pour s’exécuter sur la machine locale. Il existe une solution de contournement en utilisant Localhost comme valeur du paramètre ComputerName. Voici un exemple de cette solution de contournement.

PS51> Invoke-Command -ScriptBlock {Start-Sleep 5} -ComputerName localhost

Pour montrer le paramètre AsJob en action, l’exemple ci-dessous utilise Invoke-Command pour attendre cinq secondes, puis répète la même commande en utilisant AsJob pour montrer la différence de temps d’exécution.

PS51> Measure-Command {Invoke-Command -ScriptBlock {Start-Sleep 5}}
PS51> Measure-Command {Invoke-Command -ScriptBlock {Start-Sleep 5} -AsJob -ComputerName localhost}
Comparing speed of jobs

Runspaces : un peu comme des jobs mais plus rapides !

Jusqu’à présent, vous avez appris différentes façons d’utiliser des threads supplémentaires avec PowerShell en utilisant uniquement les commandes intégrées. Une autre option pour exécuter votre script en multithreading est d’utiliser un espace d’exécution distinct.

Les runspaces sont les zones fermées dans lesquelles les threads exécutant PowerShell fonctionnent. Alors que l’espace d’exécution utilisé avec la console PowerShell est limité à un seul thread, vous pouvez utiliser des runspaces supplémentaires pour permettre l’utilisation de threads supplémentaires.

Espace d’exécution vs PSJobs

Alors qu’un espace d’exécution et un PSJob partagent de nombreuses similitudes, il existe de grandes différences de performance. La plus grande différence entre les runspaces et les PSjobs est le temps nécessaire pour les configurer et les démonter.

Dans l’exemple de la section précédente, le PSJob créé a pris environ 150 ms pour démarrer. C’est un meilleur cas car le bloc de script pour le job ne contenait pas beaucoup de code du tout et il n’y avait pas de variables supplémentaires transmises au job.

Par opposition à la création de PSJob, un espace d’exécution est créé à l’avance. La majeure partie du temps nécessaire pour démarrer un job avec un espace d’exécution est gérée avant que le code ne soit ajouté.

Voici un exemple d’exécution de la même commande que nous avons utilisée pour le PSJob dans l’espace d’exécution à la place.

Measuring speed of job creation

En revanche, ci-dessous est le code utilisé pour la version runspace. Vous pouvez voir qu’il y a beaucoup plus de code pour exécuter la même tâche. Mais l’avantage du code supplémentaire réduit presque de 3/4 le temps, permettant à la commande de commencer à s’exécuter en 36ms au lieu de 148ms.

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})
$PowerShell.BeginInvoke()
Measuring speed of creating a runspace

Exécution des runspaces : un guide pratique

L’utilisation des runspaces peut être intimidante au début, car il n’y a plus de commandes PowerShell pour vous guider. Vous devrez gérer directement les classes .NET. Dans cette section, nous allons décomposer ce qu’il faut pour créer un runspace dans PowerShell.

Dans ce guide pratique, vous allez créer un runspace distinct de votre console PowerShell et une instance PowerShell distincte. Ensuite, vous attribuerez le nouveau runspace à la nouvelle instance PowerShell et ajouterez du code à cette instance.

Créez le runspace

La première chose que vous devez faire est de créer votre nouveau runspace. Vous le faites en utilisant la classe « runspacefactory ». Stockez cela dans une variable, comme indiqué ci-dessous, afin de pouvoir y faire référence ultérieurement.

 $Runspace = [runspacefactory]::CreateRunspace()

Maintenant que le runspace est créé, attribuez-le à une instance PowerShell pour exécuter du code PowerShell. Pour cela, vous utiliserez la classe « powershell » et, comme pour le runspace, vous devrez stocker cela dans une variable, comme indiqué ci-dessous.

 $PowerShell = [powershell]::Create()

Ensuite, ajoutez le runspace à votre instance PowerShell, ouvrez le runspace pour pouvoir exécuter du code et ajoutez votre bloc de script. Cela est illustré ci-dessous avec un bloc de script pour dormir pendant cinq secondes.

 $PowerShell.Runspace = $Runspace
 $Runspace.Open()
 $PowerShell.AddScript({Start-Sleep 5})

Exécutez le runspace

Jusqu’à présent, le bloc de script n’a toujours pas été exécuté. Tout ce qui a été fait jusqu’à présent est de tout définir pour l’espace d’exécution. Pour commencer à exécuter le bloc de script, vous avez deux options.

  • Invoke() – La méthode Invoke() exécute le bloc de script dans l’espace d’exécution, mais elle attend de revenir à la console jusqu’à ce que l’espace d’exécution termine son exécution. C’est utile pour tester pour vous assurer que votre code s’exécute correctement avant de le lâcher.
  • BeginInvoke() – Utiliser la méthode BeginInvoke() est ce que vous voulez faire pour réellement obtenir un gain de performances. Cela lancera l’exécution du bloc de script dans l’espace d’exécution et vous renverra immédiatement à la console.

Lorsque vous utilisez BeginInvoke(), stockez la sortie dans une variable car elle sera nécessaire pour voir l’état du bloc de script dans l’espace d’exécution comme indiqué ci-dessous.

$Job = $PowerShell.BeginInvoke()

Une fois que vous avez stocké la sortie de BeginInvoke() dans une variable, vous pouvez vérifier cette variable pour voir l’état de la tâche, comme indiqué ci-dessous dans la propriété IsCompleted.

the job is completed

Une autre raison pour laquelle vous devrez stocker la sortie dans une variable est que, contrairement à la méthode Invoke(), BeginInvoke() ne renverra pas automatiquement la sortie lorsque le code est terminé. Pour cela, vous devez utiliser la méthode EndInvoke() une fois qu’elle est terminée.

Dans cet exemple, il n’y aurait pas de sortie, mais pour mettre fin à l’appel, vous utiliseriez la commande ci-dessous.

$PowerShell.EndInvoke($Job)

Une fois que toutes les tâches que vous avez mises en file d’attente dans le runspace sont terminées, vous devez toujours fermer le runspace. Cela permettra au processus de collecte automatique des déchets de PowerShell de nettoyer les ressources inutilisées. Voici la commande que vous utiliseriez pour cela.

$Runspace.Close()

Utilisation des pools de runspace

Alors que l’utilisation d’un runspace améliore les performances, elle rencontre une limitation majeure d’un seul thread. C’est là que les pools de runspace brillent par leur utilisation de plusieurs threads.

Dans la section précédente, vous n’utilisiez que deux runspaces. Vous n’en avez utilisé qu’un pour la console PowerShell elle-même et celui que vous aviez créé manuellement. Les pools de runspace vous permettent d’avoir plusieurs runspaces gérés en arrière-plan à l’aide d’une seule variable.

Alors que ce comportement multi-runspace peut être réalisé avec plusieurs objets runspace, l’utilisation d’un pool de runspace facilite grandement la gestion.

Les pools de runspace diffèrent des runspaces individuels dans leur configuration. Une des différences clés est que vous définissez le nombre maximum de threads pouvant être utilisés pour le pool de runspace. Avec un seul runspace, il est limité à un seul thread, mais avec un pool, vous spécifiez le nombre maximum de threads auxquels le pool peut s’étendre.

Le nombre recommandé de threads dans un pool de runspace dépend du nombre de tâches effectuées et de la machine sur laquelle s’exécute le code. Bien qu’augmenter le nombre maximum de threads n’affecte généralement pas négativement la vitesse, vous pourriez également ne voir aucun avantage.

Démonstration de vitesse du pool de runspace

Pour montrer un exemple où un pool de runspaces est plus performant qu’un seul runspace, peut-être voulez-vous créer dix nouveaux fichiers. Si vous utilisez un seul runspace pour cette tâche, vous créeriez le premier fichier, puis passeriez au deuxième, puis au troisième, et ainsi de suite jusqu’à ce que les dix soient créés. Le scriptblock pour cet exemple pourrait ressembler à ce qui suit. Vous alimenteriez ce scriptblock avec dix noms de fichiers dans une boucle et ils seraient tous créés.

$Scriptblock = {
    param($Name)
    New-Item -Name $Name -ItemType File
}

Dans l’exemple ci-dessous, un bloc de script est défini contenant un court script qui accepte un nom et crée un fichier avec ce nom. Un pool de runspaces est créé avec un maximum de 5 threads.

Ensuite, une boucle s’exécute dix fois et à chaque fois elle attribue le numéro de l’itération à $_. Ainsi, elle aurait 1 à la première itération, 2 à la deuxième, et ainsi de suite.

La boucle crée un objet PowerShell, lui attribue le bloc de script et l’argument pour le script, puis lance le processus.

Enfin, à la fin de la boucle, elle attend que toutes les tâches en attente se terminent.

$Scriptblock = {
    param($Name)
    New-Item -Name $Name -ItemType File
}

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$RunspacePool.Open()
$Jobs = @()

1..10 | Foreach-Object {
	$PowerShell = [powershell]::Create()
	$PowerShell.RunspacePool = $RunspacePool
	$PowerShell.AddScript($ScriptBlock).AddArgument($_)
	$Jobs += $PowerShell.BeginInvoke()
}

while ($Jobs.IsCompleted -contains $false) {
	Start-Sleep 1
}

Maintenant, au lieu de créer des threads un par un, elle en crée cinq à la fois. Sans les pools de runspaces, vous auriez dû créer et gérer cinq runspaces séparés et cinq instances de Powershell séparées. Cette gestion devient rapidement un chaos.

Au lieu de cela, vous pouvez créer un pool de runspaces, une instance PowerShell, utiliser le même bloc de code et la même boucle. La différence est que le runspace s’adaptera pour utiliser tous les cinq threads tout seul.

Création de pools de runspaces

La création d’un pool de runspace est très similaire à celle du runspace créé dans la section précédente. Voici un exemple de la façon de le faire. L’ajout d’un scriptblock et l’invocation du processus sont identiques à un runspace. Comme vous pouvez le voir ci-dessous, le pool de runspace est créé avec un maximum de cinq threads.

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$RunspacePool.Open()

Comparaison des Runspaces et des Pools de Runspace pour la vitesse

Pour montrer la différence entre un runspace et un pool de runspace, créez un runspace et exécutez la commande Start-Sleep précédente. Cette fois, cependant, elle doit être exécutée 10 fois. Comme vous pouvez le voir dans le code ci-dessous, un runspace est créé qui va dormir pendant 5 secondes.

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})

1..10 | Foreach-Object {
    $Job = $PowerShell.BeginInvoke()
    while ($Job.IsCompleted -eq $false) {Start-Sleep -Milliseconds 100}
}

Notez que, puisque vous utilisez un seul runspace, vous devez attendre qu’il soit terminé avant de pouvoir commencer une autre invocation. C’est pourquoi une attente de 100 ms est ajoutée jusqu’à ce que le travail soit terminé. Bien que cela puisse être réduit, vous verrez des rendements décroissants car vous passerez plus de temps à vérifier si le travail est terminé qu’à attendre la fin du travail.

D’après l’exemple ci-dessous, vous pouvez voir qu’il a fallu environ 51 secondes pour terminer 10 ensembles de pauses de 5 secondes.

Measuring performance of creating runspaces

Maintenant, au lieu d’utiliser un seul runspace, passez à un pool de runspace. Voici le code qui va être exécuté. Vous pouvez voir qu’il y a quelques différences entre l’utilisation des deux dans le code ci-dessous lors de l’utilisation d’un pool de runspace.

$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
$Jobs = @()

1..10 | Foreach-Object {
    $PowerShell = [powershell]::Create()
    $PowerShell.RunspacePool = $RunspacePool
    $PowerShell.AddScript({Start-Sleep 5})
    $Jobs += $PowerShell.BeginInvoke()
}
while ($Jobs.IsCompleted -contains $false) {Start-Sleep -Milliseconds 100}

Comme vous pouvez le voir ci-dessous, cela se termine en un peu plus de 10 secondes, ce qui est nettement amélioré par rapport aux 51 secondes pour le runspace unique.

Comparing runspaces vs. runspace pools

Voici un résumé détaillé de la différence entre un runspace et un pool de runspace dans ces exemples.

Property Runspace Runspace Pool
Wait Delay Waiting for each job to finish before continuing to the next. Starting all of the jobs and then waiting until they have all finished.
Amount of Threads One Five
Runtime 50.8 Seconds 10.1 Seconds

Facilité d’utilisation des Runspaces avec PoshRSJob

A frequent occurrence when programming is that you will do what is more comfortable and accept the small loss in performance. This could be because it makes the code easier to write or easier to read, or it could just be your preference.

La même chose se produit avec PowerShell, où certaines personnes utilisent des PSJobs au lieu de runspaces en raison de leur facilité d’utilisation. Il existe quelques astuces pour obtenir de meilleures performances sans rendre l’utilisation trop compliquée.

Il existe un module largement utilisé appelé PoshRSJob qui contient des modules qui correspondent au style des PSJobs normaux, mais avec l’avantage supplémentaire d’utiliser des runspaces. Au lieu de devoir spécifier tout le code pour créer le runspace et l’objet PowerShell, le module PoshRSJob s’occupe de tout cela lorsque vous exécutez les commandes.

Pour installer le module, exécutez la commande ci-dessous dans une session PowerShell en tant qu’administrateur.

Install-Module PoshRSJob

Une fois le module installé, vous pouvez voir que les commandes sont les mêmes que les commandes PSJob, mais avec le préfixe RS. Au lieu de Start-Job, c’est Start-RSJob. Au lieu de Get-Job, c’est Get-RSJob.

Voici un exemple d’exécution de la même commande dans un PSJob, puis dans un RSJob. Comme vous pouvez le voir, ils ont une syntaxe et une sortie très similaires, mais elles ne sont pas tout à fait identiques.

run the same command in a PSJob and then again in an RSJob

Voici du code qui peut être utilisé pour comparer la différence de vitesse entre un PSJob et un RSJob.

Measure-Command {Start-Job -ScriptBlock {Start-Sleep 5}}
Measure-Command {Start-RSJob -ScriptBlock {Start-Sleep 5}}

Comme vous pouvez le voir ci-dessous, il y a une grande différence de vitesse car les RSJobs utilisent toujours des runspaces en dessous de la surface.

large speed difference since the RSJobs are still using runspaces below the covers

Foreach-Object -Parallel

La communauté PowerShell a souhaité une façon plus facile et intégrée de réaliser rapidement un processus multithread. C’est ainsi que le commutateur « parallel » a été développé.

Au moment de la rédaction de cet article, PowerShell 7 est encore en préversion, mais ils ont ajouté un paramètre « Parallel » à la commande « Foreach-Object ». Ce processus utilise des espaces d’exécution pour paralléliser le code et utilise le scriptblock utilisé pour « Foreach-Object » comme scriptblock pour l’espace d’exécution.

Bien que les détails soient encore en cours d’élaboration, cela pourrait être une façon plus facile d’utiliser les espaces d’exécution à l’avenir. Comme vous pouvez le voir ci-dessous, vous pouvez rapidement parcourir de nombreux ensembles de pauses.

Measure-Command {1..10 | Foreach-Object {Start-Sleep 5}}
Measure-Command {1..10 | Foreach-Object -Parallel {Start-Sleep 5}}
Looping through and measuring commands

Défis de la multi-threading

Bien que la multi-threading semble jusqu’à présent être une chose incroyable, ce n’est pas tout à fait le cas. Il existe de nombreux défis liés à la multi-threading de n’importe quel code.

Utilisation de variables

Un des plus grands et plus évidents défis de la multi-threading est que vous ne pouvez pas partager de variables sans les passer en tant qu’arguments. Il y a une exception avec une table de hachage synchronisée, mais cela sera abordé un autre jour.

Les PSJobs et les espaces d’exécution fonctionnent sans accès aux variables existantes et il n’y a aucun moyen d’interagir avec les variables utilisées dans différents espaces d’exécution depuis votre console.

Cela pose un énorme défi pour passer dynamiquement des informations à ces jobs. La réponse est différente en fonction du type de multi-threading que vous utilisez.

Pour les commandes Start-Job et Start-RSJob du module PoshRSJob, vous pouvez utiliser le paramètre ArgumentList pour fournir une liste d’objets qui seront transmis en tant que paramètres au bloc de script dans l’ordre que vous les avez listés. Voici des exemples des commandes utilisées pour les PSJobs et les RSJobs.

PSJob :

Start-Job -Scriptblock {param ($Text) Write-Output $Text} -ArgumentList "Hello world!"

RSJob :

Start-RSJob -Scriptblock {param ($Text) Write-Output $Text} -ArgumentList "Hello world!"

Les runspaces natifs ne vous offrent pas la même facilité. Au lieu de cela, vous devez utiliser la méthode AddArgument() sur l’objet PowerShell. Voici un exemple de ce que cela ressemblerait pour chaque cas.

Runspace :

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({param ($Text) Write-Output $Text})
$PowerShell.AddArgument("Hello world!")
$PowerShell.BeginInvoke()

Alors que les pools de runspaces fonctionnent de la même manière, voici un exemple de la façon d’ajouter un argument à un pool de runspaces.

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$RunspacePool.Open()
$PowerShell.AddScript({param ($Text) Write-Output $Text})
$PowerShell.AddArgument("Hello world!")
$PowerShell.BeginInvoke()

Journalisation

La multithreading introduit également des défis de journalisation. Comme chaque thread fonctionne indépendamment des autres, ils ne peuvent pas tous journaliser au même endroit. Si vous essayez de journaliser par exemple dans un fichier avec plusieurs threads, chaque fois qu’un thread écrit dans le fichier, aucun autre thread ne peut le faire. Cela pourrait ralentir votre code ou le faire échouer complètement.

À titre d’exemple, voici un code qui tente de journaliser 100 fois dans un seul fichier en utilisant 5 threads dans un pool de runspaces.

$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
1..100 | Foreach-Object {
	$PowerShell = [powershell]::Create().AddScript({'Hello' | Out-File -Append -FilePath .\Test.txt})
	$PowerShell.RunspacePool = $RunspacePool
	$PowerShell.BeginInvoke()
}
$RunspacePool.Close()

D’après la sortie, vous ne verrez aucune erreur, mais si vous regardez la taille du fichier texte, vous pouvez voir ci-dessous que tous les 100 jobs ne se sont pas terminés correctement.

Get-Content -Path .\Test.txt).Count” class=”wp-image-3241″/>
(Get-Content -Path .\Test.txt).Count

Pour contourner cela, vous pouvez journaliser dans des fichiers séparés. Cela résout le problème de verrouillage du fichier, mais vous vous retrouvez alors avec de nombreux fichiers journaux que vous devrez trier pour comprendre tout ce qui s’est passé.

Une autre alternative consiste à permettre que le timing de certaines sorties soit décalé et à ne consigner que ce qu’un travail a fait une fois terminé. Cela vous permet d’avoir tout sérialisé à travers votre session d’origine, mais vous perdez certains détails car vous ne savez pas nécessairement dans quel ordre tout s’est produit.

Résumé

Alors que le multithreading peut offrir d’énormes gains de performance, il peut aussi causer des maux de tête. Alors que certaines charges de travail en bénéficieront grandement, d’autres pas du tout. Il existe de nombreux avantages et inconvénients à utiliser le multithreading, mais s’il est utilisé correctement, vous pouvez réduire considérablement le temps d’exécution de votre code.

Lecture complémentaire

Source:
https://adamtheautomator.com/powershell-multithreading/