PS Job
Auf der Seite PS Parallel habe ich verschiedene Ansätze zur parallelen Abarbeitung von Aufträgen dargestellt und auf PS Distributed eine Sonderform zur Synchronisierung paralleler Prozesse dargestellt. Ein PowerShell-Skript kann aber auch über "Jobs" bestimmte Aufgaben quasi in den Hintergrund verlagern und danach wieder abrufen. Diese Seite beschreibt diese Funktion und die Fallstricke.
Achtung:
Ein per "Start-Job" oder mit dem Parameter "-AsJob"
gestartetes Programm ist kein "Multithreading", sondern
starte eine neue Umgebung die CPU-Last und vor allem ca
80-120Mbyte RAM pro Job belegt.
Grundprinzip eines Job
Normalerweise läuft ein PowerShell-Skript streng sequentiell von oben nach unten ab und verzichtet auf Sprünge. Sich wiederholende Sequenzen werden als Funktion ausgelagert. Es findet aber keine echte parallele Verarbeitung innerhalb des Skripts statt. Das ist natürlich störend, wenn mehrere Dinge aus Gründen der Zeitersparnis auch parallel ausgeführt werden könnten oder bestimmte Dinge länger dauern und den weiteren Fortschritt behindern.
Genau hierfür kennt PowerShell die Option einen Skript-Teil oder ein Commandlet als "Job" auszulagern. In beiden Fällen bekommt das aufrufende Skript ein Job-Objekt zurück, über welches es dann den Status und die Rückgaben auswerten kann. Hier mal ein einfaches Beispiel eines künstlich verlangsamten Jobs:
start-job -scriptblock { foreach ($count in (1..5)) { write-host "Count"; $count start-sleep -seconds 10 } }
Dieser Job läuft ca. 5x10 Sekunden und zählt einfach hoch. Der Status des Jobs während der Laufzeit kann mit Get-Job abgefragt werden. Mit Wait-Job kann ich auf das Ende warten.
Beachten Sie aber immer, dass der Job im gleichen Prozessraum des aufrufenden PowerShell-Skripts läuft. Wird das aufrufende Skript beendet, dann werden auch die Jobs einfach abgebrochen. Über Get-Job können Sie mehr Details den Job in Erfahrung bringen:
PS C:\> get-job 2| fl HasMoreData : True StatusMessage : Location : localhost Command : foreach ($count in (1..5)) { write-host "Count"; $count start-sleep -seconds 10 } JobStateInfo : Completed Finished : System.Threading.ManualResetEvent InstanceId : 0d5cb38a-d77b-4aa4-adae-7cf886077f8a Id : 2 Name : Job2 ChildJobs : {Job3} PSBeginTime : 09.06.2016 14:32:39 PSEndTime : 09.06.2016 14:33:35 PSJobTypeName : BackgroundJob Output : {} Error : {} Progress : {} Verbose : {} Debug : {} Warning : {} State : Completed
Wenn Sie nun mehrere Jobs starten, dann werden Sie sich vielleicht wundern, dass die Nummerierung immer in Zweierschritten aufsteigt. Das liegt daran, dass PowerShell neben dem eigentlichen Job noch einen zweiten Job als Child startet.
PS C:\> get-job -IncludeChildJob Id Name PSJobTypeName State HasMoreData Location -- ---- ------------- ----- ----------- -------- 2 Job2 BackgroundJob Completed True localhost 3 Job3 Completed True localhost
Die Jobliste können Sie mit "Remove-Job" wieder bereinigen. Verschiedene Commandlets erlauben ihnen den Umgang mit Jobs.
- Start-Job
https://technet.microsoft.com/de-de/library/hh849698.aspx - Get-Job
https://technet.microsoft.com/de-de/library/hh849693.aspx - Wait-Job
https://technet.microsoft.com/de-de/library/hh849735.aspx - Remove-Job
https://technet.microsoft.com/de-de/library/hh849742.aspx
Unterschied zu "-AsJob"
Viele Commandlets können ebenfalls direkt mit dem Parameter "-AsJob" zu einem Hintergrund-Job gewandelt werden. Aber auch hier ist der Einsatzbereich ein etwas anderer. Der Start eines Commandlet mit dem Parameter "-job" ist primäre dafür vorgesehen, den Aufruf mit einer Remote Powershell zu einem Job zu machen. Wer also z.B. eine Exchange Powershell oder Skype for Business PowerShell remote ausführt, der kann mit "-AsJob" den Befehl auf der entfernten Maschine als Job ausführen.
Bis PowerShell 2.0 gibt es wohl den Bug, dass solche Jobs immer ein "HasModedata = True" liefern, auch wenn es keine Rückgabe gibt.
Unterschied zu "Start-Process" und "Invoke-Command"
Sehr nahe neben "Start-Job" finden sie auch die Commandlets "Start-Process" und "Invoke-Command". Auf den ersten Blick scheinen sie ziemlich die gleiche Aufgabe zu erfüllen, Aber es gibt aber wesentliche Unterschiede.
Start-Process | Invoke-Command | Start-Job | |
---|---|---|---|
Anzeige | GUI mit Interaktion möglich |
Vordergrund |
Hintergrund ohne Interaktion |
Returnwerte | Keine Rückgabe an aufrufenden Prozess |
Keine Rückgabe an aufrufenden Prozess |
Rückgabe über "Receive-Job" |
Commandlets |
Debug-Process Get-Process Start-Process Stop-Process Wait-Process |
Invoke-Command |
Get-Job Receive-Job Remove-Job Resume-Job Start-Job Stop-Job Suspend-Job Wait-Job |
Vereinfach kann man sagen, dass ein "Job" ein Hintergrundprozess ist, den man starten, abfragen, stoppen, pausieren und wiederaufnehmen kann. Der Job wird aber nie "sichtbar" werden. Ein Prozess ist eher der Start eines anderen Skripts oder Programms als eigenständigen Windows-"Prozess".
Prozessumgebung
Interessant ist natürlich, welche "Umgebung" bei dem Prozess ankommt. Ich habe daher einfach aus dem aufrufenden Script eine Azure AD Powershell gestartet und dann im Job die Abfragen ausführen lassen:
Connect-msolservice $job = Start-job -scriptblock {get-msolUser} wait-job $job $result = Receive-Job $job $result
Die Ausführung liefert folgenden Fehler:
Der mit "Start-Job" im Hintergrund ausgeführte Skriptblock hat nicht die Laufzeitumgebung des ausführenden Scripts, sondern ist komplett "autark". Ein Versuch den Befehl mit "-asJob" in den Hintergrund zu senden geht nicht da z.B. "Get-MSOLUser" diesen Parameter gar nicht anbietet. Sie müssen also im Skriptblock eventuell benötigte Voraussetzungen erst herstellen
Das gilt übrigens auch für Extensions und SnapIns, die ggfls. neu geladen werden müssen. Wenn Sie verschiedene Jobs immer mit der gleichen Startumgebung nutzen wollen, dann ist der Parameter "-Initializationscript" interessant.
Use this parameter to prepare the
session in which the job runs. For example, you can use it
to add functions, snap-ins, and modules to the session.
Quelle:
https://technet.microsoft.com/de-de/library/hh849698.aspx
- Calling a PowerShell function in a
Start-Job script block when it’s defined in
the same script
http://stuart-moore.com/calling-a-powershell-function-in-a-start-job-script-block-when-its-defined-in-the-same-script/ - Start-Job
https://technet.microsoft.com/de-de/library/hh849698.aspx - Multithreading with Jobs in PowerShell
http://www.get-blog.com/?p=22
Rückgabe durch den Job
An dem einfachen Beispiel haben sie schon gesehen, dass der Job natürlich auch Ausgaben generieren kann. Der Jobstatus zeigt über das Property "HasMoreData" an, dass Daten abgeholt werden können. Dazu dient dann das Commandlet "Receive-Job". Interessant ist dabei die Ausgabe.
Receive-Job liefert die Ausgaben von "Write-Host" direkt an die Konsole schon beim Abrufen. Nur die Daten in der Ausgabepipeline landen in der Variablen. Wer in dem Job also Daten verarbeitet und die Ergebnisse zurückmelden will, kann z.B. Diagnosefunktionen einfach weiter per Write-Host ausgeben, die dann vom aufrufenden Script per Transcript einfach beim "Receive-Job" erfasst werden können. Ergebnisdaten hingegen sollte der Job in die Ausgabepipeline übergeben, damit Sie mit Receive-Job ausgelesen werden können.
Achtung:
Receive-Job holt nur genau einmal die Daten ab. Danach ist
die Pipeline des Jobs geleert. Beim Status sehen Sie auch,
dass "HasMoreData" dann auf False steht.
Sie können auch bei einem laufenden Job direkt die Daten in der Zwischenzeit abrufen. So können Sie z.B. den Fortschritt melden.
Die Rückgabe einfacher Strings ist ja relativ einfach. Ich wollte natürlich wissen, wie dies mit "komplexeren Objekten" aussieht. Hier meine Tests mit unterschiedlichen Rückgaben:
Scriptblock | Ergebnis | Bewertung |
---|---|---|
Ein einfacher String { [string]"string" } |
"string" |
Zurück kommt einfach ein String. Das war aber auch nicht anders zu erwarten |
Ein einfacher Integer {[int]5} |
5 |
Auch mit einem Integer funktioniert es |
Array mit gemischten Werten {@([int]1,[string]"string2")} |
1 |
Ein Array wird auch als Array zurück gegeben. Auch die Typen der einzelnen Elemente bleiben erhalten. |
Mehrere verschiedene Variablen { @([int]1,[string]"string2") [int]3; [string]"string4" } |
1 |
Zurück kommt ein Array mit vier Elementen. Wenn als mehrere Variablen im Job ausgegeben werden, dann kann mit Receive-Job ein Array mit den Einzelergebnissen erhalten werden. |
WMI-Objekt { get-process } |
Liste der Prozesse als object[] |
Auch hier ist ein relativ komplexes Objekt durch den aufrufenden Prozess abrufbar und verwertbar. |
Insoweit habe ich keine Einschränkung gefunden. Allerdings habe ich nun nicht genau z.B. den Speicherbedarf analysiert. Die Daten es Jobs müssen ja gepuffert werden und wenn diese mit "Receive-Job" abgeholt werden, könnte PowerShell diese noch mal kopieren oder einfach den Pointer auf die Variable übergeben. Meine "Jobs" sind aber eh eher kurzweilig und kein echtes Multithreading.
Rückgabe zur Laufzeit
Während der Job läuft, kann das aufrufende Skript ja weiter arbeiten. Ich habe dann einen Job gebaut, der alle paar Sekunden eine Ausgabe erzeugt. Noch während der Job aktiv ist, kann ich mit Receive-Job die Ausgaben abholen.
start-job -scriptblock { $count = 0; while ($True) { $count++; $count start-sleep -seconds 5 } }
Der Job läuft so endlos, bis ich ihn mit "Stop-Job" unsanft beende oder die aufrufende Powershell beende. Aber während er läuft, kann ich die Ausgaben einfach auslesen.
Wenn das Skript nichts neues ausgegeben hat, dann liefert "Receive-Job" einfach ein "$NULL. Allerdings ist die Aussage von "HasMoreData" in diesem Fall nicht zuverlässig.
Parameter an dem Job
Allein mit der Rückgabe an einen Job ist es nicht getan. Ich möchte auch Parameter an den Job übergeben. Start-Job sieht dabei den Parameter "-argumentlist" vor. Nebenbei kann ich damit gleich prüfen, ob der ScriptBlock auf eine im aufrufenden Skript definierte Variable auch so zugreifen kann. Das folgende Beispiel soll das erläutern:
$a=[int]1 $b=[string]"string2" start-job -argumentlist $a ` -scriptblock { param ($joba) "JobA=" + $joba "VarB=" + $b }
Die per ArgumentList übergebene Variable ist angekommen aber auf "$b" konnte der ScriptBlock nicht zugreifen.
Mir ist kein eingebauter Weg bekannt, wie ich während der Laufzeit z.B. Informationen oder Steuerdaten an den Job übergeben kann, die dieser Job dann auswerten kann. Globale Variablen oder "ByRef"-übergebene Parameter sind nicht nutzbar. Insofern müssen Sie für solche Anforderungen dann eine Prozess zu Prozess-Kommunikation per IPC, RPC, TCP, NamePipe o.ä., aufbauen
Übergabe an einen laufenden Jobs / Argumentlist
Dann hat mich interessiert, wie ich Daten an einen laufenden Job übergeben kann. Sicher könnte der Job eine TCP-Verbindung öffnen und über den Kanal angesprochen werden-. Auch wäre eine Datenübergabe über "Dateien" oder eine Pipeline möglich. Zuerst habe ich aber prüfen sollen, ob vielleicht eine Variable möglich ist, die per "[ref]" im Skriptblock definiert wird. Daher habe ich folgenden Code entworfen:
$a=[int]1 $job= start-job ` -scriptblock { param ([ref]$joba) while ($True) { [string]$joba; start-sleep -seconds 5 } } ` -argumentlist $a start-sleep -seconds 2 receive-job $job
Allerdings scheint das nicht wirklich zu funktionieren:
Die Übergabe einer Variable als "ByRef" statt "ByVal" mit der Hoffnung, dass ich so vom aufrufenden Programm auch den Inhalt in dem Job nachträglich ändern kann, ist mir nicht gelungen. Alternativen wären dann lokale Named Pipes, WebService, TCP-Connection o.ä., was natürlich auch per LocalHost funktionieren kann. Bei Netzwerkverbindungen ist natürlich die Authentifizierung zu gewährleisten:
- Interprocess Communications
https://msdn.microsoft.com/en-us/library/windows/desktop/aa365574(v=vs.85).aspx - Interprocess Communication in Powershell
https://gbegerow.wordpress.com/2012/04/09/interprocess-communication-in-powershell/ - Windows PowerShell and Named Pipes
https://rkeithhill.wordpress.com/2014/11/01/windows-powershell-and-named-pipes/
Da wohl alle Werte nach dem Parameter "-Argumentlist" als Argumente verstanden werden, sollten sie diesen Parameter als letzten Parameter verwenden.
- Passing parameter to Start-Job
http://powershell.org/forums/topic/passing-parameter-to-start-job/ - How to pass Multiple Variables as
Arguments to a Script using Start-job
http://stackoverflow.com/questions/13376030/how-to-pass-multiple-variables-as-arguments-to-a-script-using-start-job - ArgumentList parameter to Start-Job
Incomplete
https://connect.microsoft.com/PowerShell/feedback/details/563695/argumentlist-parameter-to-start-job-incomplete - Powershell - Cannot process argument
transformation
http://stackoverflow.com/questions/21524101/powershell-cannot-process-argument-transformation - pass parameters ByRef to a function
http://www.tek-tips.com/viewthread.cfm?qid=1597161
Performance
Jobs im Hintergrund stören das Hauptprogramm nicht wirklich. Allerdings habe ich schon gemerkt, dass der Start eines Job durchaus dauern kann. Sie können das selbst einmal testen. Zwischen 2-3 Sekunden dauert allein der Start eines "Write-Host".
PS C:\> measure-command {Start-Job -ScriptBlock {write-host "done"}} Days : 0 Hours : 0 Minutes : 0 Seconds : 3 Milliseconds : 329 Ticks : 33299146 TotalDays : 3,85406782407407E-05 TotalHours : 0,000924976277777778 TotalMinutes : 0,0554985766666667 TotalSeconds : 3,3299146 TotalMilliseconds : 3329,9146 PS C:\> Get-Job Id Name PSJobTypeName State HasMoreData Location -- ---- ------------- ----- ----------- -------- 2 Job2 BackgroundJob Completed True localhost PS C:\> Get-Job | fl HasMoreData : True StatusMessage : Location : localhost Command : write-host "done" JobStateInfo : Completed Finished : System.Threading.ManualResetEvent InstanceId : 3ac9e635-b25d-472d-aa3d-b841ba09a06a Id : 2 Name : Job2 ChildJobs : {Job3} PSBeginTime : 11.06.2016 15:22:00 PSEndTime : 11.06.2016 15:22:06 PSJobTypeName : BackgroundJob Output : {} Error : {} Progress : {} Verbose : {} Debug : {} Warning : {} State : Completed
Das ist schon sehr lange aber Start-Job ist dafür einfach zu nutzen. Nach der PSBeginTime und PSEndTime ist der Job selbst sogar 6 Sekunden aktiv. Starte ich dann die Jobs mehrfach, dann wird es aber schneller. Es hat den Eindruck, dass die PowerShell hier vermutlich beim ersten Start erst die entsprechenden Module laden und die Umgebung aufbauen muss.
PS C:\> measure-command {Start-Job -ScriptBlock {write-host "done"}} Days : 0 Hours : 0 Minutes : 0 Seconds : 0 Milliseconds : 686 Ticks : 6865582 TotalDays : 7,94627546296296E-06 TotalHours : 0,000190710611111111 TotalMinutes : 0,0114426366666667 TotalSeconds : 0,6865582 TotalMilliseconds : 686,5582 PS C:\> get-job 5 | fl StatusMessage : HasMoreData : True Location : localhost Runspace : System.Management.Automation.RemoteRunspace Command : write-host "done" JobStateInfo : Completed Finished : System.Threading.ManualResetEvent InstanceId : f1d145f2-36a5-446a-b1dd-86db4fe5217e Id : 5 Name : Job5 ChildJobs : {} PSBeginTime : 11.06.2016 15:24:34 PSEndTime : 11.06.2016 15:24:37 PSJobTypeName : Output : {} Error : {} Progress : {} Verbose : {} Debug : {} Warning : {} State : Completed
Der Start hat nur 0,6 Sek benötigt und der Job selbst war nach 3 Sekunden komplett fertig. Dennoch gibt es durchaus Optionen, dies bei sehr vielen Jobs effektiver zu machen.
- Using Background Runspaces Instead of
PSJobs For Better Performance
https://learn-powershell.net/2012/05/13/using-background-runspaces-instead-of-psjobs-for-better-performance/
Drosseln von Jobs
Es passiert mit Start-Job sehr schnell, dass die mehrere Jobs in den Hintergrund senden. Dabei sollten Sie aber immer an die Ressourcen ihres Servers achten. Auch wenn es verlockend ist, z.B. ein komplettes Class-C Subnetz mit 255 eigenen Jobs zu scannen, sollten Sie einen Scriptblock erst einmal mit wenigen parallelen Jobs testen. Sehr schnell überlasten Sie den Server und die Ergebnisse sind verzögert oder sogar fehlerhaft. Eine gute Idee ist es vor dem Start eines die Jobs zu zählen, die gerade aktiv sind und neue Jobs erst auf die Reise zu senden, wenn eine selbst gesetzte Zahl nicht überschritten ist. Die Anzahl der aktiven Jobs können Sie einfach wie folgt ermitteln:
(get-job -State running).count
Natürlich sollten Sie die Ergebnisse von Jobs ebenfalls zeitnah einsammeln und die erledigten Jobs dann auch bereinigen. Die Liste der erledigten Jobs, die keine Daten mehr haben ist wie folgt schnell zu erhalten und zu bereinigen:
Get-Job -HasMoreData $false -State completed | remove-job
Ansonsten werden die Jobs mit dem Ende des aufrufenden Powershell-Skripts erledigt.
Weitere Links
- PS Distributed
- PS Parallel
- PS Parameter
- PowerShell Mutex
- “HasMoreData” is true even after Receive-Job
http://stackoverflow.com/questions/10933732/hasmoredata-is-true-even-after-receive-job/17712776#17712776 - Multithreading with Jobs in PowerShell
http://www.get-blog.com/?p=22 - True Multithreading in PowerShell
http://www.get-blog.com/?p=189 - Using Background Runspaces Instead of
PSJobs For Better Performance
https://learn-powershell.net/2012/05/13/using-background-runspaces-instead-of-psjobs-for-better-performance/