Parallelverarbeitung in PowerShell — Die Wahl zwischen ForEach-Object -Parallel und Jobs
· Go Komura · PowerShell, Windows, Parallelverarbeitung, Leistungsverbesserung, Automatisierung, Betriebsoptimierung, Skript, Jobs
„Ein Skript, das 200 PCs auf Erreichbarkeit anpingt, braucht 15 Minuten pro Durchlauf.“ „Das Hashen von 100.000 Dateien unter einem Freigabeordner wird nie fertig.“ — Wächst PowerShell-Automatisierung über eine gewisse Größe hinaus, stoßen Sie unweigerlich an eine Wand der Verarbeitungszeit. Und die meiste dieser Arbeit ist vom Warten dominiert. Die CPU ist untätig; das Skript ist langsam, weil es Element für Element auf Netzwerkantworten und Festplatten-E/A wartet. Genau hier kommt Parallelität ins Spiel.
PowerShell 7 hat ForEach-Object -Parallel eingeführt und die Hürde für Parallelverarbeitung drastisch gesenkt. Es gibt jedoch einen Haken: Unbedachte Parallelisierung beschleunigt die Dinge nicht nur nicht, sie kann sie verlangsamen — und die Ergebnisse zusätzlich beschädigen. „Der Wert verschwand, als ich eine gemeinsame Variable aktualisiert habe“, „die Ausgabe kommt jedes Mal in anderer Reihenfolge zurück“, „die im Aufrufer definierte Funktion wird nicht gefunden“ — das sind alles klassische Symptome, wenn man Parallelverarbeitung verwendet, ohne zu verstehen, wie sie funktioniert.
Dieser Artikel zeichnet die in PowerShell verfügbaren Parallelisierungsoptionen nach, behandelt dann die Verwendung von ForEach-Object -Parallel und seine Fallstricke, wann Start-ThreadJob gegenüber Start-Job zum Einsatz kommt, sowie die Fälle, in denen Sie überhaupt nicht parallelisieren sollten — in einer Form, nach der Sie in der Praxis handeln können.
1. Das Wichtigste zuerst
ForEach-Object -Parallelwurde in PowerShell 7.0 hinzugefügt. Es existiert in Windows PowerShell 5.1 nicht.1- Jeder Skriptblock läuft in einem eigenen Runspace (Ausführungsumgebung). Variablen und Funktionen des Aufrufers sind nicht unverändert sichtbar. Variablen werden mit dem Gültigkeitsbereichs-Modifikator
$using:übergeben.1 - Der Standard für
-ThrottleLimitist 5. Ab PowerShell 7.1 wird ein Runspace-Pool wiederverwendet, und ThrottleLimit wird zur Größe dieses Pools. Möchten Sie jedes Mal einen frischen, verwenden Sie-UseNewRunspace.1 - Was
$using:übergibt, ist eine Referenz. Das Lesen ist sicher, aber wenn Sie sie aktualisieren möchten, brauchen Sie einen threadsicheren Typ ausSystem.Collections.Concurrent. Das gleichzeitige Aktualisieren einer gewöhnlichen Hashtable oder List beschädigt sie.1 - Parallelisierung ist keine „wird immer schneller“-Maßnahme. Die offizielle Dokumentation stellt ausdrücklich fest, dass das Parallelisieren trivialer Arbeit deutlich langsamer als normal sein kann. Sie zahlt sich bei Arbeit mit langen Wartezeiten aus, sowie bei Berechnungen, die tatsächlich von mehreren Kernen profitieren.1
- Sowohl Ausgabe- als auch Fehlerreihenfolge sind nicht deterministisch. Auch die Reihenfolge, in der Dinge in den Fehlerstrom geschrieben werden, ist zufällig, und ein abbrechender Fehler innerhalb eines Skriptblocks stoppt nur diese eine Iteration, gemeldet als
PSTaskException.1 - Sie können es mit
-AsJobals Job versenden. Beachten Sie jedoch, dass-ThrottleLimitdie Parallelität pro Job ist, sodass das Erstellen mehrerer Jobs die Zahl der gleichzeitigen Ausführungen vervielfacht.1 - Für leichtgewichtige Parallelität Start-ThreadJob verwenden; brauchen Sie Isolation, Start-Job. Start-Job läuft in einem separaten Prozess, sodass der Overhead hoch ist und Ergebnisse serialisiert zurückkommen, nachdem sie ihre Methoden verloren haben.23
- Führen Sie dieselbe Arbeit über mehrere Windows-Rechner hinweg aus, ist die implizite Parallelität von
Invoke-Commandder kürzeste Weg. Es läuft standardmäßig gegen bis zu 32 Rechner gleichzeitig.4
2. Die vier Parallelisierungsoptionen
Beginnen wir mit einer Übersicht. Es gibt grob vier Wege, in PowerShell „Dinge gleichzeitig auszuführen“.
| Option | Ausführungseinheit | Verfügbar in | Geeignet für |
|---|---|---|---|
ForEach-Object -Parallel |
Thread (Runspace) | PowerShell 7.0+1 | Dieselbe Arbeit auf jedem Element einer Sammlung. Erste Wahl |
Start-ThreadJob |
Thread (Runspace) | In PowerShell 7 gebündelt / unter 5.1 aus der Gallery installierbar2 | Eine kleine Anzahl von Aufgaben anstoßen und später sammeln |
Start-Job |
Separater Prozess | In 5.1 und 7 eingebaut3 | Arbeit, die Isolation braucht, oder die Sie in einem separaten Prozess wollen |
Invoke-Command -ComputerName |
Jeder Remote-PC | In 5.1 und 7 eingebaut4 | Dieselbe Arbeit auf mehrere Windows-Rechner verteilen |
Die praktische Entscheidung ist einfach. Wenden Sie dieselbe Arbeit auf eine große Zahl von Zielen an, verwenden Sie ForEach-Object -Parallel; sind die Ziele „mehrere Remote-PCs“, kommt Remote-Ausführung zuerst. Letzteres läuft standardmäßig gegen bis zu 32 der angegebenen Rechner gleichzeitig, sodass Sie die Parallelität nicht selbst schreiben müssen.4 Zur Einrichtung der Remote-Ausführung selbst siehe „Eine Einführung in PowerShell Remoting (WinRM)“.
Start-Job ist schwergewichtig, weil Hintergrundjobs als separater Prozess gestartet werden. Zu den Kosten des Prozessstarts kommt hinzu, dass Ergebnisse serialisiert zurückkommen, sodass die empfangenen Objekte „deserialisierte Kopien“ ohne Methoden sind.3 Start-ThreadJob dagegen läuft auf einem Thread innerhalb desselben Prozesses und ist erheblich leichter.2
3. Die Grundlagen von ForEach-Object -Parallel
Die minimale Form sieht so aus. $_ ist das aktuelle Eingabeobjekt, und äußere Variablen werden mit dem Präfix $using: referenziert.1
$timeout = 2 # Eine Variable im Aufrufer
$result = $computers | ForEach-Object -Parallel {
$name = $_
# Äußere Variablen sind ohne das Präfix $using: nicht sichtbar
$ok = Test-Connection -TargetName $name -Count 1 -TimeoutSeconds $using:timeout -Quiet
# Die Grundform ist, statt eine gemeinsame Aggregationsvariable zu aktualisieren, das Ergebnis "auszugeben"
[pscustomobject]@{
Computer = $name
Alive = $ok
CheckedAt = Get-Date
}
} -ThrottleLimit 20
$result | Where-Object { -not $_.Alive } | Format-Table
Es gibt hier ein Entwurfsprinzip, das Sie sich zu eigen machen sollten. Statt gemeinsame Variablen zu aktualisieren, sollte jeder Skriptblock sein Ergebnis „ausgeben“, und Sie sammeln alles im Aufrufer. Mit dieser Form treten Thread-Sicherheitsprobleme erst gar nicht auf. Code, der in Parallelverarbeitung Ärger verursacht, versucht fast immer, gemeinsamen Zustand zu verändern.
Müssen Sie wirklich in eine gemeinsame Sammlung sammeln, verwenden Sie einen threadsicheren Typ.1
# Das in der offiziellen Dokumentation gezeigte Muster: ConcurrentDictionary lässt sich von mehreren Threads sicher aktualisieren
$safeDict = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
Get-Process | ForEach-Object -Parallel {
$dict = $using:safeDict
# TryAdd() gibt einen booleschen Wert zurück. Verwerfen, sonst mischt sich True/False in die Ausgabe
$null = $dict.TryAdd($_.ProcessName, $_)
}
# [Gefährlich] Eine gewöhnliche Hashtable oder List<T> mit $using: zu übergeben und zu aktualisieren, ist nicht sicher
# $shared = @{} # Gleichzeitige Aktualisierungen beschädigen die interne Struktur, was zu Ausnahmen oder verlorenen Werten führt
4. Fünf Fallstricke
(1) Funktionen des Aufrufers sind nicht sichtbar
Weil ein paralleler Skriptblock in einem separaten Runspace läuft, lassen sich im Aufrufer definierte Funktionen nicht unverändert aufrufen. Der richtige Ansatz ist, die gemeinsame Logik in ein Modul auszulagern und es am Anfang des Skriptblocks zu importieren.
$items | ForEach-Object -Parallel {
Import-Module 'D:\Scripts\Modules\KsOps' -ErrorAction Stop # In jedem Runspace laden
Convert-KsRecord -Input $_
} -ThrottleLimit 8
Allerdings fallen die Kosten für das Laden des Moduls in jedem Runspace an, einmal pro Parallelitätseinheit. Bei Arbeit, die ein schwergewichtiges Modul verwendet, kann dies der Hauptgrund sein, warum der Durchsatz ein Plateau erreicht, obwohl Sie den Parallelitätsgrad erhöhen. Konventionen zur Modularisierung werden in „PowerShell-Parameterdesign und Modularisierung“ behandelt.
(2) Runspaces werden wiederverwendet, sodass Zustand übertragen werden kann
In PowerShell 7.0 wurde für jede Iteration ein neuer Runspace erstellt, aber ab 7.1 werden Runspaces standardmäßig aus einem Pool wiederverwendet.1 Das ist eine erhebliche Leistungsverbesserung, bedeutet aber, dass gesetzte Präferenzvariablen, während einer Iteration vorgenommene Änderungen des aktuellen Verzeichnisses oder geladene Module für nachfolgende Iterationen sichtbar sind, die denselben Runspace verwenden. Brauchen Sie vollständige Unabhängigkeit zwischen Iterationen, geben Sie -UseNewRunspace an (auf Kosten der Geschwindigkeit).1
(3) Weder Ausgabe- noch Fehlerreihenfolge ist garantiert
Die Reihenfolge der Parallelausführung ist nicht deterministisch. Auch die Reihenfolge, in der Dinge in den Fehlerstrom geschrieben werden, ist zufällig, und dasselbe gilt für die Warn-, Verbose- und Informationsströme.1
1..5 | ForEach-Object -Parallel {
if ($_ -eq 3) { throw "Terminating Error: $_" } # Nur diese Iteration stoppt
"Output: $_"
}
# Output: 1 / Output: 4 / Output: 2 / Output: 5 (in beliebiger Reihenfolge); Output: 3 erscheint nie
Ein abbrechender Fehler innerhalb eines Skriptblocks beendet nur diese eine Iteration, und die anderen Parallelausführungen laufen weiter. Der Fehler wird als ErrorRecord in den Fehlerstrom geschrieben, dessen FullyQualifiedErrorId PSTaskException lautet. Da das Verhalten nicht „alles stoppen, sobald auch nur eines fehlschlägt“ ist, müssen Sie Erfolg und Misserfolg über alle Elemente hinweg selbst aggregieren.1
$results = $files | ForEach-Object -Parallel {
# Innerhalb eines catch-Blocks ändert sich $_ zum ErrorRecord, daher immer das
# Eingabeobjekt vor der Verwendung in einer separaten Variable ablegen
$file = $_
try {
$hash = Get-FileHash -Path $file.FullName -Algorithm SHA256 -ErrorAction Stop
[pscustomobject]@{ Path = $file.FullName; Hash = $hash.Hash; Error = $null }
}
catch {
# Fehlschläge ebenfalls als "Ausgabe" zurückgeben und im Aufrufer sortieren
[pscustomobject]@{ Path = $file.FullName; Hash = $null; Error = $_.Exception.Message }
}
} -ThrottleLimit 8
$failed = $results | Where-Object Error
if ($failed) { Write-Warning "$($failed.Count) item(s) failed" }
(4) PipelineVariable kann nicht verwendet werden
Der gemeinsame Parameter -PipelineVariable wird in parallelen Szenarien nicht unterstützt, selbst nicht mit $using:.1 Ein Punkt, über den man beim Portieren von sequenzieller Verarbeitung stolpert.
(5) Fortschrittsanzeige und Protokolle vermischen sich
Schreiben mehrere Threads gleichzeitig Write-Progress oder eigene Protokolle, verschachteln sich die Zeilen, und Dateien konkurrieren miteinander. Statt direkt innerhalb des Skriptblocks an eine Datei anzuhängen, ist es sicherer, Protokolle als Ergebnisobjekte zurückzugeben und sie von einer einzigen Stelle im Aufrufer aus zu schreiben. Das Design von Ausgabeströmen wird in „PowerShell-Ausgabeströme und Protokolldesign“ behandelt.
5. Wie man ThrottleLimit festlegt
-ThrottleLimit ist die Anzahl gleichzeitig laufender Skriptblöcke, und der Standard ist 5.1 Richtlinien zur Festlegung gliedern sich nach der Art der Arbeit.
| Art der Arbeit | Richtlinie | Grund |
|---|---|---|
| Netzwerkwartezeiten (Erreichbarkeitsprüfungen, API-Aufrufe) | Kann größer als die Kernanzahl sein (Versuchen Sie, bei etwa 20–50 zu beginnen) | Die CPU ist größtenteils untätig. Die Einschränkung liegt auf der Gegenseite |
| Festplatten-E/A-Wartezeiten | Versuchen Sie, bei etwa 8–16 zu beginnen | Zu hoch angesetzt erhöht wahlfreie Zugriffe und kehrt sich um. Der Unterschied zwischen SSD und HDD ist groß |
| CPU-Berechnung (Hashing, Komprimierung, Konvertierung) | Etwa die Anzahl logischer Kerne | Darüber hinaus ist es nur verschwendeter Kontextwechsel |
| Die Gegenseite ist ein Geschäftsserver oder eine API | Deren Kapazität ist die Obergrenze | Ratenbegrenzungen oder Limits gleichzeitiger Verbindungen zu überschreiten macht Sie zur Ursache eines Ausfalls |
Diese letzte Zeile zählt in der Praxis am meisten. Einen Geschäftsserver in die Knie zu zwingen, um das eigene Skript zu beschleunigen, verfehlt den Zweck, setzen Sie also, wenn die Gegenseite eine interne API oder ein Dateiserver ist, Ihren Parallelitätsgrad auf „was diese aushalten können“. Zum Umgang mit API-Ratenbegrenzungen siehe „REST-APIs aus PowerShell einbinden“.
Die Dokumentation ist auch bei einem Vorbehalt zur Verwendung von -AsJob ausdrücklich: ThrottleLimit ist die Grenze pro Aufruf von ForEach-Object -Parallel, erstellen Sie also 10 Jobs, laufen „10 × ThrottleLimit“ gleichzeitig.1
6. Fälle, in denen Nicht-Parallelisieren schneller ist
Die offizielle Dokumentation geht weiter, als man erwarten könnte — neue Runspaces bringen gegenüber sequenzieller Verarbeitung erheblichen Overhead, ein triviales paralleles Skript kann deutlich langsamer als normal sein, und man sollte experimentieren, um herauszufinden, wo es tatsächlich hilft.1 Es gibt sogar ein Beispiel in der offiziellen Dokumentation, das ausdrücklich als ineffiziente Verwendung von Parallelität gekennzeichnet ist.
Die Kriterien sind einfach.
- Kurze Verarbeitungszeit pro Element (Millisekunden) → nicht parallelisieren. Zeichenfolgenverarbeitung, Hashtable-Nachschlagen und Ähnliches sind sequenziell schneller
- Wenige Elemente (einige Dutzend) → nicht parallelisieren. Der Overhead ist relativ groß
- Wartezeiten von mehreren hundert Millisekunden oder mehr pro Element → Parallelisierung lohnt sich
- Langlaufende Berechnung → mehrere Kerne helfen
Und immer vor der Entscheidung messen. Der Vergleich der sequenziellen und der parallelen Version mit Measure-Command liefert in wenigen Dutzend Sekunden eine Antwort. Messkonventionen und die allgemeine Beschleunigung von PowerShell-Skripten werden in „Wo man nachsieht, wenn ein PowerShell-Skript langsam ist“ behandelt.
# Sequenziell und parallel mit derselben Eingabe vergleichen (ein paar Mal ausführen und den stabilen Wert betrachten)
# Die parallele Seite läuft in einem separaten Runspace, daher sind im Aufrufer definierte Funktionen nicht sichtbar.
# Das macht den Vergleich bedeutungslos, also entweder ein Modul importieren oder die Arbeit inline schreiben
$seq = Measure-Command {
$items | ForEach-Object { Invoke-Work $_ }
}
$par = Measure-Command {
$items | ForEach-Object -Parallel {
Import-Module 'D:\Scripts\Modules\KsOps' # <- ohne dies erhalten Sie "Befehl nicht gefunden"
Invoke-Work $_
} -ThrottleLimit 16
}
'{0:N1}s -> {1:N1}s' -f $seq.TotalSeconds, $par.TotalSeconds
7. Die Wahl zwischen Start-ThreadJob und Start-Job
ForEach-Object -Parallel passt zur Form „dieselbe Arbeit auf viele Eingaben anwenden“, aber wenn Sie Arbeit unterschiedlicher Art gleichzeitig ausführen und die Ergebnisse später sammeln möchten, passen Jobs besser.
# Separate Aufgaben gleichzeitig ausführen und gemeinsam abwarten (Thread-Jobs = leichtgewichtig)
# Auch Thread-Jobs laufen in einem separaten Runspace, daher sind Funktionen des Aufrufers nicht sichtbar.
# Das Modul am Anfang jedes Jobs laden (oder den Skriptblock eigenständig machen)
$jobs = @(
Start-ThreadJob -Name 'AD User Inventory' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsAdUserReport
}
Start-ThreadJob -Name 'File Server Capacity' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsShareUsage
}
Start-ThreadJob -Name 'License Summary' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsLicenseSummary
}
)
# Auf den Abschluss warten und die Ergebnisse sammeln. Fehler immer mit -ErrorVariable erfassen
$reports = $jobs | Receive-Job -Wait -ErrorAction SilentlyContinue -ErrorVariable jobErrors
# Der Zustand wird nur dann Failed, wenn der Skriptblock mit einem "abbrechenden Fehler" gestorben ist.
# Ein Job, der mit einem nicht abbrechenden Fehler wie Write-Error endete, bleibt Completed,
# sodass allein der Blick auf den Zustand "Fehler traten auf, wird aber als Erfolg behandelt" bedeutet
$failed = @($jobs | Where-Object { $_.State -eq 'Failed' })
$jobs | Remove-Job -Force
if ($failed.Count -gt 0 -or @($jobErrors).Count -gt 0) {
throw "Errors occurred during parallel execution: $(($jobErrors | ForEach-Object { $_.Exception.Message }) -join ' / ')"
}
Der Grund, hier -AutoRemoveJob nicht zu verwenden, ist, dass der Job unmittelbar nach dem Sammeln verschwindet, sodass Sie nicht mehr prüfen können, welcher fehlgeschlagen ist. Fehler von Receive-Job sind standardmäßig nicht abbrechende Fehler, schreiben Sie also nichts, erhalten Sie die schlimmstmögliche Form: „manche sind fehlgeschlagen, aber nur der erfolgreiche Teil kam zurück, und es sieht aus, als wäre alles abgeschlossen“. Bei Inventar- und Berichtsautomatisierung bleibt diese Art von stillem Verlust als falsche Zahlen bestehen.
Sie würden sich in Fällen wie diesen für Start-Job (die prozessisolierte Variante) entscheiden.3
- Arbeit, bei der Sie den aufrufenden Prozess nicht mitreißen möchten (etwa der Aufruf einer nativen DLL, die abstürzen könnte)
- Arbeit, die eine andere Prozessumgebung braucht (Sie möchten unter einer anderen Kultureinstellung oder anderen Umgebungsvariablen laufen)
- Arbeit, bei der Sie die Ausführungsumgebung selbst trennen möchten, etwa 32-Bit gegenüber 64-Bit
Umgekehrt gilt: Trifft nichts davon zu, ist Start-Job genau um den Betrag seines Overheads und seiner Serialisierungseinschränkungen im Nachteil. Die Tatsache, dass deserialisierte Objekte keine Methoden haben, rächt sich auch in der nachgelagerten Verarbeitung.3
8. In Umgebungen mit nur Windows PowerShell 5.1
-Parallel ist unter 5.1 nicht verfügbar. Es gibt drei realistische Optionen.
Start-ThreadJob(das ModulThreadJobaus der PowerShell Gallery installieren) — leichtgewichtige Parallelität ist auch unter 5.1 verfügbar. Zu Konventionen für die interne Verteilung siehe „Interne Verteilung und Aktualisierung von PowerShell-Modulen“2Invoke-Command -ComputerName— sind die Ziele mehrere Windows-Rechner, liefert dies allein Parallelität (standardmäßig 32 Rechner gleichzeitig)4- PowerShell 7 installieren — 5.1 und 7 können koexistieren, sodass es eine realistische Wahl ist, nur die schwere Arbeit unter 7 laufen zu lassen
Die dritte Option erweist sich am Ende oft als die günstigste, und wir empfehlen „genau dieses Stück Arbeit unter 7 laufen lassen“ gegenüber „aufwendigen 5.1-Code um der Parallelität willen schreiben“. Unsere Überlegungen zu Koexistenz und Migration finden sich in „Die Unterschiede zwischen Windows PowerShell 5.1 und PowerShell 7“.
9. Praktische Faustregeln (Entscheidungstabelle)
| Was Sie tun möchten | Wahl | Hinweise |
|---|---|---|
| Dieselbe Arbeit auf vielen Zielen (Erreichbarkeitsprüfungen, Hashing, API-Aufrufe) | ForEach-Object -Parallel |
Nur PowerShell 7. So entwerfen, dass Ergebnisse als Ausgabe zurückgegeben werden1 |
| Dieselbe Arbeit auf mehreren Windows-Rechnern | Invoke-Command -ComputerName |
Standardmäßig 32 Rechner gleichzeitig. Keine eigene Parallelität nötig4 |
| Arbeit unterschiedlicher Art gleichzeitig ausführen | Start-ThreadJob |
Leichtgewichtig. Mit Receive-Job -Wait sammeln2 |
| Prozessisolation erforderlich | Start-Job |
Schwergewichtig. Ergebnisse werden deserialisiert3 |
| Ergebnisse an einer Stelle sammeln | Als Ausgabe zurückgeben / ConcurrentDictionary |
Das gleichzeitige Aktualisieren einer gewöhnlichen Hashtable oder List ist nicht erlaubt1 |
| Jedes Element ist leicht, oder es gibt nur wenige Elemente | Nicht parallelisieren | Overhead macht es stattdessen langsamer1 |
| Die Gegenseite ist ein Geschäftsserver oder eine API | ThrottleLimit nach deren Kapazität festlegen | Ratenbegrenzungen und Limits gleichzeitiger Verbindungen sind die faktische Obergrenze |
| Unabhängigkeit zwischen Iterationen ist essenziell | -UseNewRunspace |
Vermeidet durch Wiederverwendung übertragenen Zustand (auf Kosten der Geschwindigkeit)1 |
10. Zusammenfassung
ForEach-Object -Parallelist eine Funktion ab PowerShell 7.0, die jeden Skriptblock in einem eigenen Runspace ausführt. Die Grundform ist$using:für die Variablen des Aufrufers sowie die Modularisierung von Funktionen und deren Import.- Entwerfen Sie jede Iteration so, dass sie ihr Ergebnis ausgibt und im Aufrufer aggregiert, statt gemeinsame Variablen zu aktualisieren, vermeiden Sie Thread-Sicherheitsprobleme nahezu vollständig. Müssen Sie wirklich teilen, verwenden Sie die Concurrent-Typen.
- Der Standard für ThrottleLimit ist 5. Setzen Sie ihn hoch für von Wartezeiten dominierte Arbeit, etwa auf Kernanzahl für CPU-Arbeit, und bei Arbeit mit einem anderen System ist dessen Kapazität die Obergrenze.
- Ausgabe- und Fehlerreihenfolge sind nicht deterministisch, und ein abbrechender Fehler stoppt nur diese eine Iteration. Sie müssen Erfolg und Misserfolg über alle Elemente hinweg selbst aggregieren.
- Parallelisierung ist kein Allheilmittel. Bei leichter Arbeit oder wenigen Elementen macht der Overhead es langsamer. Vergleichen Sie vor der Übernahme immer mit
Measure-Commandgegen die sequenzielle Version. - In 5.1-Umgebungen
Start-ThreadJob; für Remote-ArbeitInvoke-Command; und reicht das immer noch nicht, ist die Installation von PowerShell 7 der realistische Weg.
Beispielcode zum Download
Der in diesem Artikel behandelte Code liegt als direkt ausführbares Paket vor. Er enthält praktische Bausteine für ForEach-Object -Parallel und Start-ThreadJob.
Beispielcode herunterladen (zip)
Die Beispiele in diesem Artikel wurden tatsächlich unter PowerShell 7.6 ausgeführt und verifiziert (8 Pester-Tests). Führen Sie das im ZIP enthaltene Invoke-SampleTests.ps1 aus, um dieselbe Verifizierung auf Ihrem eigenen Rechner zu reproduzieren.
# Syntaxanalyse + statische Analyse + Pester-Tests
./Invoke-SampleTests.ps1
Konfigurationswerte (Pfade, Servernamen, Mandanten-IDs und so weiter) sind Beispiele. Führen Sie sie nicht unverändert in einer Produktivumgebung aus — passen Sie sie an Ihre eigene Umgebung an.
Verwandte Artikel
- Die Unterschiede zwischen Windows PowerShell 5.1 und PowerShell 7 — Ein praktischer Leitfaden zur Migration hauseigener Skripte
- Eine Einführung in PowerShell Remoting (WinRM) — Mehrere Windows-Rechner auf einmal verwalten
- PowerShell-Parameterdesign und Modularisierung — Von „einem funktionierenden Skript“ zu „einem übergabefähigen Skript“
- Einen Dateiserver mit PowerShell erfassen — Kapazitätsanalysen und Prüfungen der Zugriffsrechte (ACL)
- PowerShell-Fehlerbehandlung und Wiederholungsdesign — Von der try/catch-Falle bis zu Exitcodes und Best Practices für Wiederholungen
Verwandte Beratungsbereiche
KomuraSoft LLC übernimmt die Beschleunigung langlaufender Betriebsskripte, die Überprüfung von Automatisierungsentwürfen mit Parallelverarbeitung sowie die Untersuchung von Störungen wie „die Ergebnisse waren nach der Parallelisierung falsch“.
- Technische Beratung und Design-Review
- Fehleranalyse und Ursachenermittlung
- Migration und Weiterverwendung von Legacy-Assets
- Kontakt
Referenzlinks
</content>
-
Microsoft Learn, ForEach-Object. Zum Parametersatz -Parallel, hinzugefügt in PowerShell 7.0, dazu, dass jeder Skriptblock in einem neuen Runspace läuft, zur Übergabe von Variablen über den Gültigkeitsbereichs-Modifikator $using:, dazu, dass -ThrottleLimit standardmäßig 5 beträgt und zur Größe des Runspace-Pools wird, dazu, dass Runspaces ab 7.1 wiederverwendet werden, wobei -UseNewRunspace zum Erstellen frischer Runspaces verfügbar ist, zum Verhalten von -AsJob und -TimeoutSeconds, zur Notwendigkeit threadsicherer Typen wie derer in System.Collections.Concurrent beim Aktualisieren einer mit $using: übergebenen Referenz, zur nicht deterministischen Ausgabereihenfolge nicht abbrechender Fehler, dazu, dass abbrechende Fehler nur die einzelne Parallelinstanz mit der FullyQualifiedErrorId PSTaskException beenden, dazu, dass PipelineVariable in parallelen Szenarien nicht unterstützt wird, zum großen Overhead neuer Runspaces, der triviale Arbeit stattdessen langsamer macht, sowie dazu, dass ThrottleLimit bei Verwendung von -AsJob eine Grenze pro Job ist. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22
-
Microsoft Learn, Start-ThreadJob. Dazu, dass Start-ThreadJob einen Skriptblock auf einem separaten Thread innerhalb desselben Prozesses statt in einem separaten Prozess ausführt, leichter als Start-Job ist, sich mit den Standard-Job-Cmdlets (Receive-Job und Ähnliches) verwalten lässt, sowie zur Steuerung der Nebenläufigkeit mit -ThrottleLimit. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Jobs. Dazu, dass Hintergrundjobs Befehle asynchron in einem neuen Prozess ausführen, zu Job-Operationen über Start-Job, Get-Job, Receive-Job und Wait-Job, sowie dazu, dass Job-Ergebnisse über Serialisierung zurückgegeben werden. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Invoke-Command. Dazu, dass sich derselbe Befehl gegen mehrere mit -ComputerName angegebene Computer ausführen lässt, dazu, dass -ThrottleLimit die Anzahl gleichzeitiger Verbindungen mit einem Standard von 32 begrenzt, sowie zur Hintergrundausführung mit -AsJob. ↩ ↩2 ↩3 ↩4 ↩5
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Wo Sie nachsehen sollten, wenn ein PowerShell-Skript langsam ist — Arrays, Pipelines und Abgleich
Die klassischen Ursachen langsamer PowerShell-Skripte im Überblick. Warum += bei einem Array O(n²) ist, der Unterschied zwischen Pipeline...
Schluss mit Write-Host — PowerShells Ausgabeströme und Log-Design
Wie Sie zwischen den sechs Ausgabeströmen von PowerShell wählen, welche Probleme Write-Host mit sich bringt und wo es tatsächlich hingehö...
Externe EXEs aus PowerShell korrekt aufrufen — Die Fallstricke bei Argument-Quoting, Exitcodes und Zeichensalat
Rufen Sie robocopy oder eine hauseigene EXE aus PowerShell auf, und die Argumente zerbrechen, der Exitcode ist nicht verfügbar, und die A...
Zugangsdaten in PowerShell sicher handhaben — Klartext-Passwörter aus Ihren Skripten verbannen
Ein praktischer Leitfaden, um Klartext-Passwörter aus PowerShell-Skripten zu entfernen und sicher zu speichern: was SecureString wirklich...
Parameterdesign und Modularisierung für PowerShell-Skripte — Von der „funktionierenden“ zur „übergabefähigen“ Skript
Eine schrittweise Vorgehensweise, um ein PowerShell-Skript auf eine Qualität zu heben, die Sie an andere übergeben können. Behandelt den ...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Kann ich ForEach-Object -Parallel in Windows PowerShell 5.1 verwenden?
- Nein. -Parallel ist ein Parametersatz, der in PowerShell 7.0 hinzugefügt wurde und in 5.1 schlicht nicht existiert. Um unter 5.1 zu parallelisieren, haben Sie die Optionen Start-ThreadJob aus dem im PowerShell Gallery verfügbaren Modul ThreadJob, das eingebaute Start-Job (prozessisoliert und schwergewichtig), oder den eigenen Aufbau eines RunspacePool. Geht es darum, hauseigene Skripte zu beschleunigen, ist die Installation von PowerShell 7 und die Verwendung von -Parallel für die Wartbarkeit besser, als aufwendigen Parallel-Code für 5.1 zu schreiben.
- Ich habe mein Skript parallelisiert, und es wurde langsamer. Warum?
- Weil der Overhead der Parallelausführung größer ist als die Arbeit selbst. ForEach-Object -Parallel führt jeden Skriptblock in einem eigenen Runspace aus, was gegenüber sequenzieller Verarbeitung einen erheblichen Overhead mit sich bringt, und die offizielle Dokumentation stellt ausdrücklich fest, dass ein triviales paralleles Skript deutlich langsamer als normal sein kann. Ist jedes Element in wenigen Millisekunden erledigt, oder haben Sie nur einige Dutzend Elemente, ist es meist schneller, nicht zu parallelisieren. Parallelität zahlt sich bei Arbeit mit langen Netzwerk- oder Datei-E/A-Wartezeiten aus, sowie bei Berechnungen, die tatsächlich von mehreren Kernen profitieren.
- Kann ich innerhalb eines parallelen Skriptblocks in eine mit $using: übergebene Variable schreiben?
- Das Lesen des referenzierten Werts ist sicher, das Schreiben jedoch nicht, sofern das Ziel kein threadsicherer Typ ist. $using: ist ein Mechanismus, um eine Variablenreferenz vom aufrufenden Thread an den Thread jedes Skriptblocks zu übergeben, und weil mehrere Threads gleichzeitig darauf zugreifen, beschädigt eine Aktualisierung einer gewöhnlichen Hashtable oder List diese. Möchten Sie aggregierte Ergebnisse sammeln, verwenden Sie einen threadsicheren Typ aus dem Namensraum System.Collections.Concurrent wie ConcurrentDictionary oder ConcurrentBag — oder entwerfen Sie besser jeden Skriptblock so, dass er seinen Wert ausgibt und im Aufrufer empfängt.
- Was ist der richtige Wert für ThrottleLimit?
- Das hängt von der Art der Arbeit ab. Der Standard ist 5. Von Netzwerk- oder Datei-E/A-Wartezeiten dominierte Arbeit (Erreichbarkeitsprüfungen gegen Server, API-Aufrufe und Ähnliches) kann von Werten profitieren, die höher als die Anzahl der CPU-Kerne liegen. Rechenintensive Arbeit, die die CPU sättigt, wird hingegen allein durch Konkurrenz langsamer, sobald Sie deutlich über die Kernanzahl hinausgehen. Ist die Gegenseite ein Geschäftsserver oder eine API, ist die Obergrenze nicht nur Ihre eigene Bequemlichkeit — auch deren Grenzen für gleichzeitige Verbindungen und Ratenbegrenzungen sind die Obergrenze. Der sichere Ansatz ist, zunächst mit dem Standardwert 5 zu messen, ihn dann zu verdoppeln und zu prüfen, ob das tatsächlich hilft.
- Ich kann meine eigenen Funktionen nicht aus einem parallelen Skriptblock heraus aufrufen.
- Ein paralleler Skriptblock läuft in einem vom Aufrufer getrennten Runspace, sodass im aufrufenden Gültigkeitsbereich definierte Funktionen und Variablen nicht unverändert sichtbar sind. Es gibt zwei Lösungen: die gemeinsame Logik in ein Modul (.psm1) auslagern und am Anfang des Skriptblocks per Import-Module laden, oder die Funktionsdefinition als Text mit $using: übergeben und innerhalb des Skriptblocks neu definieren. Für die Wartbarkeit wird Ersteres empfohlen. Beachten Sie jedoch, dass das Laden des Moduls in jedem Runspace Kosten verursacht; ist das Modul schwergewichtig, stößt eine Erhöhung des Parallelitätsgrads an eine Obergrenze.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.