Question A propos des runspaces sous PS v1

Plus d'informations
il y a 14 ans 2 mois #5226 par Laurent Dardenne
Cette insertion fonctionne, son temps d'exécution est très court et il n'y a qu'un seul écrivain. Les risques de conflit sont donc quasi inexistants.
Le plus souvent l'intérêt d'un traitement en tâche de fond, couplé avec une Winform, est qu'il peuple automatiquement une telle liste, pendant ce temps on fait autre chose.
Sinon il y a peu d'intérêts à attendre un job, puis à peupler la listbox...

C'est ici que le mais intervient, le peuplement de la listbox fonctionne mais ne respecte pas les règles concernant
la manipulation de contrôle à partir de threads .
Comme le reconnait Bruce Payette dans ce post .
Bien que ce post date de 2006 rien n'a changé de ce coté là.

Le thread qui exécute la fenêtre n'est pas le même que celui de la session PowerShell, il nous faut mettre en place une approche thread-safe.
Mais vous pouvez aussi faire comme voulez si cela vous chante, bien qu'a un moment donné...

On peut vérifier la nécessité d'appliquer ces règles en interrogeant la propriété InvokeRequired. Au préalable modifiez la ligne suivante dans le script de création de la form :
[code:1]
#Dans le thread de la form
$Form1.Add_Shown({Write-host \"$($Form1.InvokeRequired)\";$Form1.Activate()})
[/code:1]
Ensuite on crée un second thread
[code:1]
#Dans le thread de PS
$Form1.InvokeRequired
$RS2=New-RunSpace {
#Dans le thread d'un second runspace
$Form1.InvokeRequired
}
$RS2.Runspace.SessionStateProxy.SetVariable(\"Form1\", $Form1)
#Affiche la form
$RS2.InvokeAsync($RS2.Command)
$RS2.Results
[/code:1]
L'affichage de cette propriété renvoie $false lors de l'appel dans le thread de la form et $true lors des deux autres appels dans des threads différents.
Dans la documentation de la propriété InvokeRequired , il y est dit qu'elle renvoie :

true si le Handle du contrôle a été créé sur un thread différent du thread d'appel (ce qui indique que vous devez effectuer les appels au contrôle par l'intermédiaire d'une méthode Invoke) ; sinon false.

Bien que le composant soit créé dans la session PowerShell, son handle n'est pas pour autant renseigner.
Cette notion de handle intervient lorsque le composant est affiché, c'est à dire dés qu'il utilise des ressources système. Pour rappel, schématiquement, la création d'un objet utilise dans un premier temps uniquement de la mémoire :

#Extrait du SDK
Control.Handle, propriété
Obtient le handle de fenêtre auquel le contrôle est lié.

Notes:
La valeur de la propriété Handle est un HWND Windows. Si le handle n'a pas encore été créé, toute référence à cette propriété imposera sa création.

Pour le peuplement en tâche de fond prenons le scénarios suivant :
-on crée la fenêtre,
-on lance un second runspace créant une liste de fichier à insérer dans la listbox,
-pendant ce temps on clique 2-3 fois par seconde sur le bouton \"Ajouter des données\".

Pour réduire le code j'ai crée quelques fonctions dédiées à la création de la configuration.
On extrait le nom des fonctions à partir du fichier créant la forme, fichier généré à l'aide du script ConvertTo-Form.
[code:1]
.\"$pwd\PkgRS-ScriptConfigurationEntry.ps1\"
.\"$pwd\TestFrmRunSpace1.ps1\"

#
Recopie une fonction existante dans une configuration de Runspace
$ConfigurationRS = New-RunspaceConfiguration
Select-FunctionNameFromScript TestFrmRunSpace1.ps1 |
New-ScriptConfigurationEntry|
Add-ScriptConfigurationEntry -Configuration $ConfigurationRS

$RSShowDialog=New-RunSpace -InteractWithHost {
#Ce runspace appelle la Form déclarée dans la session PS
#cette function est bloquante
$Form1.ShowDialog()
} $configurationRS

$RSShowDialog.Runspace.SessionStateProxy.SetVariable(\"Form1\", $Form1)
#On insére la listbox car le code associé au clic du bouton \"Ajout\", référence cette variable.
$RSShowDialog.Runspace.SessionStateProxy.SetVariable(\"lstBxInformations\", $lstBxInformations)
#Affiche la form
$RSShowDialog.InvokeAsync($RSShowDialog.Command)

$RSAddData=New-RunSpace -InteractWithHost {
$Fichiers=Dir c:\windows -rec|%{$_.Fullname}
$lstBxInformations.Items.AddRange($Fichiers)
}
$RSAddData.Runspace.SessionStateProxy.SetVariable(\"lstBxInformations\", $lstBxInformations)
#Affiche la form
$RSAddData.InvokeAsync($RSAddData.Command)
[/code:1]
Pour mes essais le parcours du répertoire
[code:1]
measure-command {$a=Dir c:\windows -rec|%{$_.Fullname}}
[/code:1]
renvoie 22156 fichiers et s'exécute en 937 millisecondes.
Une fois ce scénario déroulé, on peut constater par
[code:1]
$lstBxInformations.Items|? {$_ -match \"^bouton\"}
[/code:1]
que les insertions dans la liste sont désynchronisées :

Bouton Count= 0
Bouton Count= 1
Bouton Count= 2
Bouton Count= 4533
Bouton Count= 12052
Bouton Count= 20717

On souhaite que l'insertion du tableau dans la liste devrait se faire en une fois :

Bouton Count= 0
Bouton Count= 1
Bouton Count= 2
Bouton Count= 22156+3+1
Bouton Count= 22159+1
Bouton Count= 22160+1

On pourrait s'attendre à ce que cette opération soit atomique, c'est à dire qu'elle accède aux données de manière exclusive, mais il n'en est rien.
On pourrait utiliser un tableau \"synchronisé\" :
[code:1]
#début identique
#...
#Affiche la form
$RSShowDialog.InvokeAsync($RSShowDialog.Command)

$Fichiers = [Collections.Arraylist]::«»Synchronized((new-object Collections.Arraylist))

$RSAddData=New-RunSpace -InteractWithHost {
Dir c:\windows -rec|%{$_.Fullname}|%{[void]$Fichiers.Add($_)}
$lstBxInformations.Items.AddRange($Fichiers.ToArray())
}
$RSAddData.Runspace.SessionStateProxy.SetVariable(\"lstBxInformations\", $lstBxInformations)
$RSAddData.Runspace.SessionStateProxy.SetVariable(\"Fichiers\", $Data)

#Affiche la form
$RSAddData.InvokeAsync($RSAddData.Command)
[/code:1]
mais cela ne fonctionne pas mieux.
Appliquons \"la règle\" qui veut qu'on exécute le code manipulant un contrôle dans le thread qui exécute la fenêtre (ShowDialog)
[code:1]

$Fichiers = [Collections.Arraylist]::«»Synchronized((new-object Collections.Arraylist))
#injecte la collection dans le runspace ayant crée la fenêtre
$RSShowDialog.Runspace.SessionStateProxy.SetVariable(\"Fichiers\", $Data)

#Affiche la form
$RSShowDialog.InvokeAsync($RSShowDialog.Command)

#On cast un scriptblock en un délégué EventHandler(sender,args)
#Rappel: $this est égal au paramètre sender (object)
#Ce code est exécuté dans un autre thread que celui de la session primaire de Powershell,
#à savoir dans celui du runspace ayant affiché la forme.
#$this référence le composant indiqué dans l'appel à invoke : @($lstBxInformations , [eventargs]::empty)
[System.EventHandler] $EventHandler = {
Write-host \"[EventHandler:ID de runspace= $(([System.Management.Automation.Runspaces.Runspace]::«»DefaultRunSpace).InstanceId)]\"
Write-host \"[RSAddData] Begin\"
Dir c:\windows -rec|%{$_.Fullname}|%{[void]$Fichiers.Add($_)}
Write-host \"[RSAddData] End\"
$this.Items.AddRange($Fichiers.ToArray())
}


#Permet de manipuler un contrôle depuis un autre thread que celui qui l'a crée.
#Invoke exécute, de manière SYNCHRONE, le délégué spécifié dans le thread qui détient
#le handle de fenêtre sous-jacent au contrôle manipulé, ici $lstbxInformations.
#C'est à dire dans cette exemple le thread du runspace dans lequel on a exécuté ShowDialog.
#C'est dans ce runspace que l'appel à l'EventHandler utilise le handle de sa fenêtre (son parent).
# Tant que l'on a pas exécuté ShowDialog $form1 n'a pas de handle de fenêtre, et lors de la fermeture de la fenêtre le handle n'est
# plus valide (la fenêtre n'existe plus).
#
#Puisque le code de l'eventHandler référence le composant $lstBxInformationsest exécuté dan
$lstBxInformations.Invoke($EventHandler, @($lstBxInformations , [eventargs]::empty))
[/code:1]
En plus claire,
dans le runspace par défaut de PowerShell :
- on crée $RSShowDialog et $EventHandler
- on insére les variables utilisées directement ($Form1) ou celle \"invokées\" ($lstbxInformations)
- on exécute explicitement le runspace
- On exécute $lstBxInformations.Invoke

dans le runspace RSShowDialog :
- On exécute explicitement $Form1.ShowDialog
- est exécuté implicitement le code de $Eventhanlder.

Le code de la méthode Invoke() se débrouille avec les trheads, une tâche de moins à prendre en charge :-)

Le résultat ressemblant à :
[code:1]
$lstBxInformations.Items|? {$_ -match \"^bouton\"}
Bouton Count= 0
Bouton Count= 1
Bouton Count= 22158 (22156+2)
Bouton Count= 22159 +1
[/code:1]
On a bien un résultat cohérent mais la form se fige le temps du traitement car la construction du tableau est désormais bloquante (Invoke est synchrone, pour l'asynchrone voir BeginInvoke()... ).
Ce n'est pas le comportement multithread attendu, puisque maintenant c'est le code de l'EventHandler qui bloque l'interface utilisateur, on tourne en rond :-)
Enfin, on a avancé un peu avec l'appel à Invoke(), le code est désormais exécuté dans le bon thread.

Pour que le peuplement de la collection ne bloque pas l'interface il nous faut l'exécuter en tâche de fond, une de plus :
[code:1]
.\"$pwd\PkgRS-ScriptConfigurationEntry.ps1\"
.\"$pwd\TestFrmRunSpace1.ps1\"

#
Recopie une fonction existante dans une configuration de Runspace
$ConfigurationRS = New-RunspaceConfiguration
Select-FunctionNameFromScript TestFrmRunSpace1.ps1 |
New-ScriptConfigurationEntry|
Add-ScriptConfigurationEntry -Configuration $ConfigurationRS

$RSShowDialog=New-RunSpace -InteractWithHost {
$Form1.ShowDialog()
} $configurationRS

$RSBuildData=New-RunSpace -InteractWithHost {
Dir c:\windows -rec|%{$_.Fullname}|%{[void]$Fichiers.Add($_)}

[System.EventHandler] $EventHandler = { $this.Items.AddRange(($Fichiers.ToArray())) }

#appel asynchrone
$lstBxInformations.Invoke($EventHandler, @($lstBxInformations , [eventargs]::empty))
}

$Fichiers = [Collections.Arraylist]::«»Synchronized((new-object Collections.Arraylist))
#Usage explicite de Form1
$RSShowDialog.Runspace.SessionStateProxy.SetVariable(\"Form1\", $Form1)

#Usage explicite et implicite de lstBxInformations
$RSShowDialog.Runspace.SessionStateProxy.SetVariable(\"lstBxInformations\", $lstBxInformations)
#Usage explicite de lstBxInformations
$RSBuildData.Runspace.SessionStateProxy.SetVariable(\"lstBxInformations\", $lstBxInformations)

#Usage implicite de Fichiers
$RSShowDialog.Runspace.SessionStateProxy.SetVariable(\"Fichiers\", $Fichiers)
#Usage explicite de Fichiers
$RSBuildData.Runspace.SessionStateProxy.SetVariable(\"Fichiers\", $Fichiers)

$RSShowDialog,$RSBuildData|% {$_.InvokeAsync($_.Command)}
[/code:1]
En plus claire,
dans le runspace par défaut de PowerShell :
- on crée $RSShowDialog et $RSBuildData
- on crée et \"protége\" le tableau $Fichiers
- on insére dans chaque runspace les variables utilisées explicitement ($Form1) ou celle implicitement ($lstbxInformations)
- on exécute explicitement les runspaces

dans le runspace RSShowDialog :
- On exécute explicitement $Form1.ShowDialog (le runspace RSShowDialog est bloqué tant qu'on ne clôt pas la fenêtre ( appel à close() )
- est exécuté implicitement le code de $Eventhanlder déclaré dans le runspace RSBuilData

dans le runspace RSBuilData :
- on construit le tableau $Fichiers et $EventHandler
- On exécute explicitement $lstBxInformations.Invoke (le runspace RSBuilData est bloqué tant que le code de l'eventhandler n'est pas terminé)

Le code de la méthode Invoke() se débrouille avec les trheads, une tâche de moins à prendre en charge :-)
Avec ce code on a bien une insertion atomique et une interface qui reste accessible, sauf un bref instant pendant l'insertion (dépend de la taille du tableau).
Une fois le code débarrassé des commentaires, reste une vingtaine de lignes pour réaliser ce scénario.
Le code en soi n'est pas complexe, c'est tout les comportements implicites autour du traitement en tâche de fond qui posent problèmes.

Une utilisation simple, c'est à dire sans winform, sans partage de données et sans traitement à synchroniser, rendra déjà de grand service.

Enfin sachez que les runspaces partagent le même domaine d'application que PowerShell, on peut donc charger un assembly dans un runspace et l'utiliser dans un autre.
On retrouve la liste des assemblies ainsi :
[code:1]
[AppDomain]::CurrentDomain.GetAssemblies()
[/code:1]
En revanche les cmdlets additionnels, par exemple ceux de PowerShell Community Extensions, ne sont pas accessible, bien que l'assembly soit lui partagé.
On doit les insérer via la collection cmdlets de la configuration du runspace.

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 14 ans 2 mois #5228 par Laurent Dardenne
Je joins le fichier du package additionnel.

L'archive contient 3 scripts proposant des approches différentes autour de l'usage des runspaces et des traitements en tâche de fond.

Sur le blog de Arnoud Jansveld, auteur de Split-Job, on trouve ce commentaire :

Cesar June 10th, 2009 3:25 pm
Nick,
The script rocks! I am using it to collect inventory on ~1200 Pcs. The vbscripts handling it prior were getting out of control. It replaced 2 scripts, one of them with around 2500 lines of code.

Thanks!

:)

La pièce jointe PkgRS_ScriptConfigurationEntry.zip est absente ou indisponible


Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 14 ans 2 mois #5233 par Laurent Dardenne
Voici une petite astuce pour terminer, à partir de la session PS, une tâche au sein d'une boucle :
[code:1]
$RS1=New-RunSpace -interactWithHost {
#Le pipeline exécuté en tâche de fond ne connait pas le runspace dans lequel il tourne
write-host \"In process\"
$isContinue=$true
While ($isContinue )
{
Sleep -m 500
$isContinue=$executionContextProxy.SessionState.PsVariable.Get(\"isContinue\"«»).Value
}
write-host \"End\"
}# SB for New-RunSpace

#On insère un nouvel objet contenant une 'copie'
#$RS1.Runspace.SessionStateProxy.SetVariable(\"isContinue\",$isContinue)
$RS1.InvokeAsync($RS1.Command);
#le process est en cours d'exécution on ne peut pas insérer de variable
#$RS1.Runspace.SessionStateProxy.SetVariable(\"isContinue\"=$False)
$isContinue=$False
[/code:1]
Comme dans le runspace la variable $isContinue est recopiée, son contenu, modifé dans PS, n'est pas actualisé.
On doit donc relire la variable à chaque passage dans le corps de la boucle, c'est un léger inconvénient.
De plus, une fois le runspace démarré on ne peut plus y injecter de variable.

On peut utiliser $RS1.Stop() mais dans ce cas si le code après le while contient des instructions de finalisation elle ne seront pas exécutées.

Ce dont on a besoin c'est de manipuler l'adresse de la variable $isContinue, PowerShell propose le type [REF] qui convient à cet usage.
Testons si cela fonctionne :
[code:1]
Function Pause($Message=\"Pressez une touche pour continuer...\"«»)
{
Write-Host -NoNewLine $Message
$null = $Host.UI.RawUI.ReadKey(\"NoEcho,IncludeKeyDown\"«»)
Write-Host \"\"
}

$RS1=New-RunSpace -interactWithHost {
write-host \"In process\"
#Get() récupère un objet de type PSVariable,
#Get().Value récupère l'objet encapsulé, une référence
$isContinue=$executionContextProxy.SessionState.PsVariable.Get(\"isContinue\"«»).Value

#L'objet encapsulé est une référence pointant sur
#la variable $Stop de la session PS.
#On accède au contenu de la référence via le champ Value
#On a donc ici deux indirections.
While ($isContinue.Value )
{
write-host ($isContinue)
write-host ($isContinue.Value)
Sleep 4
}
write-host \"End\"
}# SB for New-RunSpace

#Flag d'arrêt
#On type la variable, valeur autorisée $True,$False.
#Sinon on peut créer une variable contrainte
#(Get-Variable stop).Attributes.Add((New-object System.Management.Automation.ValidateNotNullAttribute))
#(Get-Variable stop).Attributes.Add((New-object System.Management.Automation.ValidateRangeAttribute min,Max
[boolean]$Stop=$True

#Référence sur le flag d'arrêt
#On empêche la modification de la référence
set-variable -name isContinue -value ([ref]$Stop) -option readOnly -scope global -description \"Référence (pointeur) sur `$Stop\"

$RS1.InvokeAsync($RS1.Command)

Pause
#On ne peut pas supprimer la référence mais l'objet référencé peut l'être, son champ value peut donc valoir $null.
$Stop=$False
[/code:1]
Ainsi la modification effectué dans la session PS est bien répercuté dans le runspace, reste que l'usage de référence nécessite d'être rigoureux dans un tel contexte.

L'affichage d'une winform dans un runspace évite de bloquer la console, comme il existe d'autre cas, par exemple la méthode WaitForNextEvent() d'un objet de type ManagementEventWatcher, il est tout à fait possible de surveiller en tâche de fond une ressource Windows et ce à l'aide de WMI :
[code:1]
#
RS WatchProcess (via WMI)
#Flag d'arrêt
[boolean]$StopRSWatchProcess=$False
#Référence sur le flag d'arrêt
Set-Variable -name isRSWatchProcess -value ([ref]$StopRSWatchProcess) -option readOnly -scope global -description \"Pointeur sur `$StopRSWatchProcess\"

$RSWatchProcess=New-RunSpace -interactWithHost {
#Lit une variable dans le runspace ayant crée ce code, c'est à dire PowerShell.exe
$Stop=$executionContextProxy.SessionState.PsVariable.Get(\"isRSWatchProcess\"«»).Value
if ($Stop.Value)
{return}

$query =New-object System.Management.WqlEventQuery(
\"__InstanceCreationEvent\",
[TimeSpan]\"0:0:1\",
'TargetInstance isa \"Win32_Process\"')
$watcher =new-object System.Management.ManagementEventWatcher
$watcher.Query = $query
$watcher.Start()

While ($Stop.Value -eq $False) {
$Event = $watcher.WaitForNextEvent()
Write-host $($Stop.Value)
#Ajoute la date de création, format WMI
Write-Host $(\"Le processus {0} a été créé. Son path est : {1}\" -F $Event.TargetInstance.Name, $Event.TargetInstance.ExecutablePath)
} #While
#Annule l'abonnement
$Watcher.Stop()
#Libére les ressources
$Watcher.Dispose()
} #$RSWatchProcess
[/code:1]
Un petit inconvénient, l'affectation de la valeur $True à la variable $StopRSWatchProcess ne sera effective qu'une fois un nouvel événement reçu.
[code:1]
$RSWatchProcess.InvokeAsync($RSWatchProcess.Command);
[diagnostics.process]::«»start(\"cmd\",\"/C ping ServerInconnu\"«»)
$StopRSWatchProcess=$true
# Le runspace à de forte chance d'être en attente d'un événement
Pause
#Déclenche un événement pris en charge dans le runspace
#Comme $isContinue vaut $True on quitte la boucle,
#puis on termine le runspace
[diagnostics.process]::«»start(\"cmd\",\"/C ping ServerInconnu\"«»)
[/code:1]
Sous PS v1 l'absence d'une gestion d'événements fait vraiment défaut.
Par exemple on aurait pu envoyer un message au runspace lui demandant de se terminer, en retour il nous avertirait de la fin d'exécution du code qu'il héberge, etc, etc.
Il y a sûrement d'autres techniques à étudier, mais tout ceci nous a permis d'approfondir cette notion de runspace, son usage et les problèmes qui y sont associés.

Pour terminer, je viens de vérifier sous la version 2 de PS, un job (start-job) est totalement différent dans l'usage.
Un job exécute un scriptblock en tâche de fond, et puis c'est tout !
Il n'existe pas de Create-Job, on manipule une tâche et pas un runspace ni un pipeline, même si en interne PS utilise des runspaces évolués prenant en charge le remoting.
Je n'ai pas encore trouvé comment le réexécuter, si toutefois c'est possible.

Toujours dans la version 2, il existe une classe nommée System.Management.Automation.PowerShell qui ressemble beaucoup à l'objet Runspace créé par le script d'origine.
Bien que son fonctionnement ne soit peut être pas tout à fait le même, c'est à étudier.

Allez, vous lirez bien un dernier exemple pour la route ;-)
[code:1]
#POWERSHELL V2
.\"$pwd\TestFrmRunSpace1.ps1\"

$ps =[System.Management.Automation.PowerShell]::Create()
#Erreur : pas de commande
$Ps.Invoke()
$Ps.Streams
$e=$Ps.Streams.Error[0]
#$Ps.Streams.Error affichera toujours l'erreur[0],
#même si la liste en contient plusieurs

#Write-properties $e

$ps.AddScript('$Form1.ShowDialog()')
#Erreur : variable à $null
$Ps.Invoke()
$Ps.Streams

$ps.Runspace.SessionStateProxy.SetVariable(\"Form1\",$Form1)
#OK!
# Ici pas besoin d'insérer les functions utilisées par la form.
#ça marche tout seul !!!!
$Result=$Ps.Invoke() #Bloquant
$Result
$Ps.Streams
$ps.InvocationStateInfo

# Exécution asynchrone
#$ResultAsync=$Ps.BeginInvoke()
# ...
[/code:1]

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 14 ans 2 mois #5329 par Laurent Dardenne
Une petite correction à propos du verrouillage de données de type référence, c'est-à-dire des objets.
Dans un premier temps je comptais utiliser la classe Monitor pour verrouiller les objets partagés. J'ai donc essayé le code suivant dans la console:
[code:1]
$Locker= New-Object System.Object
[System.Threading.Monitor]::Enter($Locker)
[System.Threading.Monitor]::Exit($locker)
[/code:1]
Mais la dernière instruction provoque une exception :

Exception lors de l'appel de « Exit » avec « 1 » argument(s) : « La méthode de synchronisation de l'objet a été appelée à partir d'un bloc de code non synchronisé. »
Au niveau de ligne : 1 Caractère : 33
+ [System.Threading.Monitor]::Exit( <<<< $locker)

Sur le moment je préférais avancer sur le sujet tout en sachant que les mutex pouvaient être utilisés pour verrouiller des données.
Ce problème est tout simplement dû, comme indiqué dans les premiers posts, au fait que la console utilise un pool de thread pour exécuter des instructions dans un pipeline. Donc un thread différent pour chaque instruction, du coup l'appel à [System.Threading.Monitor]::Exit s'exécute dans un thread différent de celui de l'instruction [System.Threading.Monitor]::Enter.
Dans ce cas la méthode statique Monitor.Exit déclenche l'exception SynchronizationLockException: Le thread en cours ne possède pas le verrou pour l'objet spécifié.

Sous PowerShell, puisque \"la notion de thread n'y est pas persistante\", on doit donc utiliser les instructions liée au thread(je pose un verrou, je le libére), dans la même portée. C'est à dire un script, une fonction, un scriptblock.

Si on respecte cette contrainte, l'usage de la classe System.Threading.Monitor, comme d'autres, est possible.
[code:1]
$Valeur1=10
$Valeur1=10
$Valeur2=2
#Crée l'objet à verrouiller
$Locker= New-Object System.Object

$RS1=New-RunSpace -InteractWithHost {
#On doit impérativement libérer le verrou
trap {[System.Threading.Monitor]::Exit($Locker);write-host \"EXCEPTION RS1:$($_.exception.message)\";Break}
$host.ui.WriteLine(\"Début de RS1\"«»)

#RS1 démarre avant RS2, on essaie d'avoir un accès concurrent
sleep -m 50

$Val1=$executionContextProxy.SessionState.PsVariable.Get(\"Valeur1\"«»)
$Val2=$executionContextProxy.SessionState.PsVariable.Get(\"Valeur2\"«»)
$Locker=$executionContextProxy.SessionState.PsVariable.Get(\"Locker\"«»).Value
$host.ui.WriteLine(\"RS1 Monitor.Enter\"«»)
#Verrouille la variable $Locker du runspace de la session PowerShell
[System.Threading.Monitor]::Enter($Locker)
$host.ui.WriteLine(\"RS1 suite du traitement\"«»)
if ($val2.value -ne 0)
{ write-host \"$($val1.value / $val2.value)\" }
else
{ write-host \"Division impossible dans RS1\" }

$val2.value= 0;
#Déverrouille la variable $Locker du runspace de la session PowerShell
[System.Threading.Monitor]::Exit($locker)
$host.ui.WriteLine(\"RS1 Monitor.Exit\"«»)
} #RS1

$RS2=New-RunSpace -InteractWithHost {
trap {[System.Threading.Monitor]::Exit($Locker);write-host \"EXCEPTION RS2:$($_.exception.message)\";Break}
$host.ui.WriteLine(\"Début de RS2\"«»)
$Val1=$executionContextProxy.SessionState.PsVariable.Get(\"Valeur1\"«»)
$Val2=$executionContextProxy.SessionState.PsVariable.Get(\"Valeur2\"«»)
$Locker=$executionContextProxy.SessionState.PsVariable.Get(\"Locker\"«»).Value
$host.ui.WriteLine(\"RS2 Monitor.Enter\"«»)
[System.Threading.Monitor]::Enter($Locker)
$host.ui.WriteLine(\"RS2 suite du traitement\"«»)
if ($val2.value -ne 0)
{ write-host \"$($val1.value / $val2.value)\" }
else
{ write-host \"Division impossible dans RS2\" }
$val2.value= 0;
#on souhaite visualiser l'attente de verrou
sleep -m 100
[System.Threading.Monitor]::Exit($Locker)
$host.ui.WriteLine(\"RS2 Monitor.Exit\"«»)
}# RS2
cls
$RS1,$RS2|% {$_.InvokeAsync($_.Command)}
sleep -m 50
$valeur1
$valeur2
$RS1,$RS2|% {\"State :{0} Exception : {1}\" –F $_.LastPipelineState,$_.LastException.Message}
[/code:1]
Il s'avére qu'il est préférable d'utiliser un mutex pour ce type de besoin .<br><br>Message édité par: Laurent Dardenne, à: 20/09/09 19:10

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 14 ans 2 mois #5352 par Laurent Dardenne
Si on souhaite exécuter plusieurs runspaces et attendre que chaque traitement soit terminé avant de poursuivre le script principal, on a besoin d’un point de synchronisation.
Le pooling est une possibilité, mais la classe AutoResetEvent est préférable pour cette mise en œuvre.

Le principe est le suivant :
-On crée un runspace, on lui associe un événement de synchronisation de thread, une fois le traitement terminé on émet un signal.

-Le script principal se place en veille et attend un signal en provenance de chaque runspace (WaitAll), une fois ceci fait le script continuera sont exécution.

[code:1]
Write-host \&quot;[PS] Debut\&quot;

$sbCreateRS={
New-RunSpace {
trap
#En cas d'erreur on averti 'WaitAll' de
#la fin d'exécution du runspace
{$Event.Set();Throw $_}

#Récupére les arguments
# 1 arguments -&gt; $args =1 scalaire
# n arguments -&gt; $args =1 tableaux
$args=$input|% {$_}

Write-host \&quot;[RS$args] Debut\&quot;
Sleep ($args*5)
#Notification de l'événement
#Emet un signal
$Event.Set()
Write-host \&quot;[RS$args] Fin\&quot;
} -InteractWithHost
}

$sbCreateEWH ={New-object System.Threading.EventWaitHandle($False,[System.Threading.EventResetMode]::AutoReset)}

#Crée les runspaces
$RS1,$RS2=1..2| % {&amp;$sbCreateRS}

#Crée un tableau d'événements de notification
#Un par runspace utilisé
[System.Threading.EventWaitHandle[]]$Events=@((&amp;$sbCreateEWH),(&amp;$sbCreateEWH))

$RS1.Runspace.SessionStateProxy.SetVariable(\&quot;Event\&quot;, $Events[0])
$RS2.Runspace.SessionStateProxy.SetVariable(\&quot;Event\&quot;, $Events[1])

$RS1.InvokeAsync($RS1.Command,1)
$RS2.InvokeAsync($RS2.Command,2)

Write-host \&quot;[PS] Attend la fin de RS1 et RS2\&quot;

#Chaque thread doit déclencher un event
# A tester en mode STA!
[void][System.Threading.AutoResetEvent]::WaitAll($Events)

#Pas d'erreurs dans les runspaces ?
#TrueForAll
$Succes=&amp;{
$RS1,$RS2|
Where {$All++;$_.LastPipelineState -eq \&quot;Completed\&quot;}|
Foreach-Object -begin {$All,$Count=0,0} -process {[void]$count++} –end {$Count -eq $All}
}

Write-host \&quot;[PS] Fin\&quot;
$Events|% {$_.Close()}
[/code:1]<br><br>Message édité par: Laurent Dardenne, à: 25/09/09 15:25

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 14 ans 1 mois #5472 par Laurent Dardenne
Un tutoriel sur le sujet, il reprend une bonne partie des posts précédents.

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Temps de génération de la page : 0.097 secondes
Propulsé par Kunena