Irgendwann werden die meisten Menschen auf das Problem stoßen, dass ein grundlegendes PowerShell-Skript einfach zu langsam ist, um es zu lösen. Dies könnte das Sammeln von Daten von vielen Computern in Ihrem Netzwerk oder das Erstellen vieler neuer Benutzer in Active Directory auf einmal sein. Das sind beides gute Beispiele dafür, wo eine höhere Rechenleistung dazu führen würde, dass Ihr Code schneller ausgeführt wird. Lassen Sie uns sehen, wie wir das mit PowerShell-Multithreading lösen können!
Die Standard-PowerShell-Sitzung ist single-threaded. Sie führt einen Befehl aus und wenn er abgeschlossen ist, geht sie zum nächsten Befehl über. Das ist schön, da es alles wiederholbar hält und nicht viele Ressourcen verwendet. Aber was ist, wenn die durchgeführten Aktionen voneinander unabhängig sind und Sie die CPU-Ressourcen übrig haben? In diesem Fall wird es Zeit, über Multithreading nachzudenken.
In diesem Artikel lernen Sie verschiedene PowerShell-Multithreading-Techniken kennen und erfahren, wie Sie mehrere Datenströme gleichzeitig verarbeiten können, aber über dieselbe Konsole gesteuert.
Verständnis von PowerShell-Multithreading
Multithreading ist eine Möglichkeit, mehr als einen Befehl gleichzeitig auszuführen. Während PowerShell normalerweise einen einzelnen Thread verwendet, gibt es viele Möglichkeiten, mehr als einen Thread zu verwenden, um Ihren Code zu parallelisieren.
Der Hauptvorteil von Multithreading besteht darin, die Laufzeit des Codes zu verringern. Diese Zeitverkürzung geht jedoch mit einem höheren Bedarf an Rechenleistung einher. Bei der Verwendung von Multithreading werden viele Aktionen gleichzeitig durchgeführt, was mehr Systemressourcen erfordert.
Was ist, wenn Sie zum Beispiel einen neuen Benutzer im Active Directory erstellen möchten? In diesem Beispiel gibt es nichts, was mehrere Threads erfordert, da nur ein Befehl ausgeführt wird. Dies ändert sich jedoch, wenn Sie 1000 neue Benutzer erstellen möchten.
Ohne Multithreading würden Sie den Befehl New-ADUser
1000 Mal ausführen, um alle Benutzer zu erstellen. Nehmen wir an, es dauert drei Sekunden, um einen neuen Benutzer zu erstellen. Um alle 1000 Benutzer zu erstellen, würde es knapp unter einer Stunde dauern. Anstatt einen Thread für 1000 Befehle zu verwenden, könnten Sie stattdessen 100 Threads verwenden, von denen jeder zehn Befehle ausführt. Jetzt dauert es weniger als eine Minute anstatt etwa 50 Minuten!
Beachten Sie, dass Sie keine perfekte Skalierung sehen werden. Das Starten und Beenden von Elementen im Code dauert einige Zeit. Bei Verwendung eines einzelnen Threads muss PowerShell den Code ausführen und ist fertig. Bei mehreren Threads wird der ursprüngliche Thread, der zum Ausführen Ihrer Konsole verwendet wird, verwendet, um die anderen Threads zu verwalten. An einem bestimmten Punkt ist dieser ursprüngliche Thread voll ausgelastet, um alle anderen Threads in Schach zu halten.
Voraussetzungen für das PowerShell-Multithreading
In diesem Artikel lernen Sie, wie das PowerShell-Multithreading praxisnah funktioniert. Wenn Sie mitmachen möchten, finden Sie unten einige Dinge, die Sie benötigen, sowie einige Details zur verwendeten Umgebung.
- Windows PowerShell-Version 3 oder höher – Alles, sofern nicht explizit angegeben, funktioniert der gezeigte Code in Windows PowerShell-Version 3 oder höher. Für die Beispiele wird Windows PowerShell-Version 5.1 verwendet.
- Ersatz-CPU und Speicher – Sie benötigen zumindest etwas zusätzliche CPU und Speicherplatz, um mit PowerShell zu parallelisieren. Wenn Ihnen dies nicht zur Verfügung steht, werden Sie möglicherweise keinen Leistungsvorteil sehen.
Priorität Nr. 1: Beheben Sie Ihren Code!
Bevor Sie sich in das Beschleunigen Ihrer Skripte mit PowerShell-Multithreading stürzen, sollten Sie einige Vorbereitungsarbeiten erledigen. Zuerst sollten Sie Ihren Code optimieren.
Obwohl Sie Ihrem Code mehr Ressourcen zur Verfügung stellen können, um ihn schneller ausführen zu lassen, bringt Multithreading eine Menge zusätzlicher Komplexität mit sich. Wenn es Möglichkeiten gibt, Ihren Code vor dem Multithreading zu beschleunigen, sollten diese zuerst durchgeführt werden.
Identifizierung von Engpässen
Einer der ersten Schritte beim Parallelisieren Ihres Codes besteht darin, herauszufinden, was ihn verlangsamt. Der Code könnte langsam sein, aufgrund von schlechter Logik oder zusätzlichen Schleifen, bei denen Sie einige Änderungen vornehmen können, um eine schnellere Ausführung vor dem Multithreading zu ermöglichen.
Ein Beispiel für eine häufige Möglichkeit, Ihren Code zu beschleunigen, besteht darin, Ihr Filtern nach links zu verschieben. Wenn Sie mit einer Vielzahl von Daten interagieren, sollten alle Filterungen, die Sie vornehmen möchten, um die Datenmenge zu verringern, so früh wie möglich durchgeführt werden. Im Folgenden finden Sie ein Beispielcode, um die CPU-Auslastung des svchost-Prozesses zu ermitteln.
Das folgende Beispiel liest alle laufenden Prozesse aus und filtert dann einen einzelnen Prozess (svchost) heraus. Anschließend wird die CPU-Eigenschaft ausgewählt und sichergestellt, dass der Wert nicht null ist.
Vergleichen Sie den obigen Code mit dem folgenden Beispiel. Unten ist ein weiteres Beispiel für Code, der die gleiche Ausgabe hat, aber anders angeordnet ist. Beachten Sie, dass der folgende Code einfacher ist und die gesamte Logik möglichst links vom Pipe-Symbol platziert. Dadurch wird verhindert, dass Get-Process
Prozesse zurückgibt, die nicht relevant sind.
Unten ist der Zeitunterschied für das Ausführen der beiden oben genannten Zeilen. Während der Unterschied von 117ms bei einmaliger Ausführung dieses Codes nicht spürbar sein wird, summiert er sich, wenn er tausendmal ausgeführt wird.

Verwenden von Thread-sicherem Code
Stellen Sie sicher, dass Ihr Code „thread-sicher“ ist. Der Begriff „thread-sicher“ bezieht sich darauf, ob ein Thread Code ausführt, ein anderer Thread den gleichen Code gleichzeitig ausführen kann, ohne Konflikte zu verursachen.
Zum Beispiel ist das Schreiben in dieselbe Datei in zwei verschiedenen Threads nicht thread-sicher, da nicht bekannt ist, was zuerst zur Datei hinzugefügt werden soll. Das Lesen aus einer Datei in zwei Threads ist jedoch thread-sicher, da die Datei nicht geändert wird. Beide Threads erhalten die gleiche Ausgabe.
Das Problem mit PowerShell-Multithreading-Code, der nicht thread-sicher ist, besteht darin, dass inkonsistente Ergebnisse auftreten können. Manchmal funktioniert es gut, weil die Threads zufällig richtig zeitlich abgestimmt sind, um keine Konflikte zu verursachen. In anderen Fällen gibt es Konflikte, und das Problem ist schwierig zu diagnostizieren, da die Fehler inkonsistent auftreten.
Wenn Sie nur zwei oder drei Aufgaben gleichzeitig ausführen, kann es passieren, dass sie genau richtig aufeinander abgestimmt sind und alle zu unterschiedlichen Zeiten in die Datei schreiben. Wenn Sie dann den Code auf 20 oder 30 Aufgaben skalieren, sinkt die Wahrscheinlichkeit, dass mindestens zwei der Aufgaben gleichzeitig schreiben möchten, erheblich.
Parallele Ausführung mit PSJobs
Eine der einfachsten Möglichkeiten, ein Skript zu mehrfädig zu machen, ist mit PSJobs. PSJobs haben Cmdlets, die im Modul Microsoft.PowerShell.Core enthalten sind. Das Modul Microsoft.PowerShell.Core ist in allen Versionen von PowerShell seit Version 3 enthalten. Die Befehle in diesem Modul ermöglichen es Ihnen, Code im Hintergrund auszuführen, während Sie gleichzeitig anderen Code im Vordergrund ausführen. Unten finden Sie eine Liste aller verfügbaren Befehle.

Verfolgung Ihrer Jobs
Alle PSJobs befinden sich in einem von elf Zuständen. Diese Zustände sind die Art und Weise, wie PowerShell die Jobs verwaltet.
Unten finden Sie eine Liste der häufigsten Zustände, in denen sich ein Job befinden kann.
- Abgeschlossen – Der Job ist abgeschlossen und die Ausgabedaten können abgerufen oder der Job entfernt werden.
- Wird ausgeführt – Der Job wird derzeit ausgeführt und kann nicht ohne erzwungenes Anhalten des Jobs entfernt werden. Die Ausgabe kann auch noch nicht abgerufen werden.
- Blockiert – Der Job läuft noch, aber der Host wird aufgefordert, Informationen bereitzustellen, bevor er fortgesetzt werden kann.
- Fehler – Bei der Ausführung des Auftrags ist ein Abbruchfehler aufgetreten.
Um den Status eines gestarteten Auftrags abzurufen, verwenden Sie den Befehl Get-Job
. Dieser Befehl ruft alle Attribute Ihrer Aufträge ab.
Unten sehen Sie die Ausgabe für einen Auftrag, bei dem der Status Abgeschlossen ist. Im folgenden Beispiel wird der Code Start-Sleep 5
in einem Auftrag mit dem Befehl Start-Job
ausgeführt. Der Status dieses Auftrags wird dann mit dem Befehl Get-Job
zurückgegeben.

Wenn der Auftragsstatus Abgeschlossen lautet, bedeutet dies, dass der Code im Skriptblock ausgeführt und beendet wurde. Sie können auch sehen, dass die Eigenschaft HasMoreData
auf False
gesetzt ist. Dies bedeutet, dass nach Abschluss des Auftrags keine Ausgabe vorhanden war.
Unten sehen Sie ein Beispiel für einige der anderen Zustände, die zur Beschreibung von Aufträgen verwendet werden. Aus der Befehl
-Spalte geht hervor, dass einige dieser Aufträge nicht abgeschlossen wurden, beispielsweise weil versucht wurde, abc
Sekunden zu warten, was zu einem fehlgeschlagenen Auftrag führte.

Neue Aufträge erstellen
Wie Sie oben gesehen haben, ermöglicht es der Befehl Start-Job
, einen neuen Auftrag zu erstellen, der Code im Auftrag ausführt. Wenn Sie einen Auftrag erstellen, geben Sie einen Skriptblock an, der für den Auftrag verwendet wird. Der PSJob erstellt dann einen Auftrag mit einer eindeutigen ID-Nummer und startet die Ausführung des Auftrags.
Der Hauptvorteil hierbei ist, dass der Befehl Start-Job
weniger Zeit benötigt als das Skriptblock, den wir verwenden. Wie auf dem untenstehenden Bild zu sehen ist, dauerte der Befehl nicht fünf Sekunden, sondern nur 0,15 Sekunden, um den Job zu starten.

Der Grund, warum der gleiche Code in einem Bruchteil der Zeit ausgeführt werden konnte, liegt darin, dass er im Hintergrund als PSJob ausgeführt wurde. Es dauerte lediglich 0,15 Sekunden, um den Code im Hintergrund einzurichten und auszuführen, anstatt ihn im Vordergrund auszuführen und tatsächlich fünf Sekunden zu warten.
Abrufen der Jobausgabe
Manchmal gibt der Code innerhalb des Jobs eine Ausgabe zurück. Sie können die Ausgabe dieses Codes mit dem Befehl Receive-Job
abrufen. Der Befehl Receive-Job
akzeptiert einen PSJob als Eingabe und schreibt dann die Ausgabe des Jobs in die Konsole. Alles, was der Job während seiner Ausführung ausgegeben hat, wurde gespeichert, sodass bei Abruf des Jobs der gesamte Inhalt ausgegeben wird, der zu diesem Zeitpunkt gespeichert war.
Ein Beispiel dafür ist die Ausführung des folgenden Codes. Dies erstellt und startet einen Job, der Hello World in die Ausgabe schreibt. Anschließend wird die Ausgabe des Jobs abgerufen und in der Konsole ausgegeben.

Erstellen von geplanten Jobs
Eine weitere Möglichkeit, mit PSJobs zu interagieren, besteht darin, einen geplanten Job zu verwenden. Geplante Jobs ähneln einer Windows-geplanten Aufgabe, die mit Task Scheduler konfiguriert werden kann. Geplante Jobs ermöglichen es Ihnen, komplexe PowerShell-Skriptblöcke einfach in einer geplanten Aufgabe zu planen. Mit einem geplanten Job können Sie einen PSJob im Hintergrund basierend auf Triggern ausführen.
Job-Triggers
Job-Triggers können bestimmte Zeiten, Benutzeranmeldungen, Systemstarts und viele andere Ereignisse sein. Sie können auch festlegen, dass die Trigger in Intervallen wiederholt werden. All diese Trigger werden mit dem Befehl New-JobTrigger
definiert. Dieser Befehl wird verwendet, um einen Trigger festzulegen, der den geplanten Job ausführt. Ein geplanter Job ohne Trigger muss manuell gestartet werden, aber jeder Job kann viele Trigger haben.
Zusätzlich zu einem Trigger haben Sie immer noch einen Skriptblock, wie er bei einem normalen PSJob verwendet wird. Wenn Sie sowohl den Trigger als auch den Skriptblock haben, verwenden Sie den Befehl Register-ScheduledJob
, um den Job zu erstellen, wie im nächsten Abschnitt gezeigt. Dieser Befehl wird verwendet, um Attribute des geplanten Jobs festzulegen, wie z.B. den auszuführenden Skriptblock und Triggers, die mit dem Befehl New-JobTrigger
erstellt wurden.
Demo
Vielleicht müssen Sie PowerShell-Code jedes Mal ausführen, wenn sich jemand an einem Computer anmeldet. Dafür können Sie einen geplanten Job erstellen.
Um dies zu tun, definieren Sie zuerst einen Trigger mit New-JobTrigger
und definieren Sie den geplanten Job wie unten gezeigt. Dieser geplante Job schreibt jedes Mal, wenn sich jemand anmeldet, eine Zeile in eine Protokolldatei.
Nachdem Sie die oben genannten Befehle ausgeführt haben, erhalten Sie eine ähnliche Ausgabe wie beim Erstellen eines neuen Jobs, die die Job-ID, das Skriptblock und einige andere Attribute anzeigt, wie unten gezeigt.

Nach einigen Anmeldeversuchen können Sie anhand des folgenden Screenshots sehen, dass die Versuche protokolliert wurden.

Nutzung des Parameters AsJob
Eine andere Möglichkeit, Jobs zu verwenden, besteht darin, den eingebauten Parameter AsJob
bei vielen PowerShell-Befehlen zu verwenden. Da es viele verschiedene Befehle gibt, können Sie sie alle mit Get-Command
finden, wie unten gezeigt.
Einer der verbreitetsten Befehle ist Invoke-Command
. Normalerweise wird dieser Befehl beim Ausführen sofort ausgeführt. Während einige Befehle sofort zurückkehren und Sie mit dem Fortfahren fortfahren können, warten andere, bis der Befehl abgeschlossen ist.
Die Verwendung des Parameters AsJob
tut genau das, was es verspricht, und führt den ausgeführten Befehl als Job aus, anstatt ihn synchron in der Konsole auszuführen.
Während AsJob
in den meisten Fällen für die lokale Maschine verwendet werden kann, hat Invoke-Command
keine native Option, um auf der lokalen Maschine ausgeführt zu werden. Es gibt einen Workaround, indem Localhost als Wert für den Parameter ComputerName
verwendet wird. Im Folgenden finden Sie ein Beispiel für diesen Workaround.
Um den Parameter AsJob
in Aktion zu zeigen, verwendet das unten stehende Beispiel Invoke-Command
, um fünf Sekunden lang zu warten und dann den gleichen Befehl mit AsJob
zu wiederholen, um den Unterschied in den Ausführungszeiten aufzuzeigen.

Runspaces: Sozusagen wie Jobs, aber schneller!
Bis jetzt haben Sie verschiedene Möglichkeiten kennengelernt, um zusätzliche Threads in PowerShell mithilfe der integrierten Befehle zu verwenden. Eine weitere Möglichkeit, Ihr Skript parallel auszuführen, besteht darin, einen separaten Runspace zu verwenden.
Runspaces sind der begrenzte Bereich, in dem die Threads, die PowerShell ausführen, arbeiten. Während der Runspace, der mit der PowerShell-Konsole verwendet wird, auf einen einzelnen Thread beschränkt ist, können Sie zusätzliche Runspaces verwenden, um zusätzliche Threads zu ermöglichen.
Runspace vs. PSJobs
Obwohl ein Runspace und ein PSJob viele Ähnlichkeiten aufweisen, gibt es einige große Unterschiede in der Leistung. Der größte Unterschied zwischen Runspaces und PSJobs liegt in der Zeit, die zum Einrichten und Beenden benötigt wird.
Im Beispiel aus dem vorherigen Abschnitt dauerte das Erstellen des PSJobs etwa 150 ms. Dies ist ungefähr der bestmögliche Fall, da das Skriptblock für den Job nicht viel Code enthielt und keine zusätzlichen Variablen an den Job übergeben wurden.
Im Gegensatz zur Erstellung eines PSJobs wird ein Runspace im Voraus erstellt. Der größte Teil der Zeit, die zum Starten eines Runspace-Jobs benötigt wird, wird behandelt, bevor Code hinzugefügt wurde.
Im Folgenden finden Sie ein Beispiel für die Ausführung des gleichen Befehls, den wir zuvor für den PSJob verwendet haben, stattdessen im Runspace.

Im Gegensatz dazu finden Sie unten den Code für die Runspace-Version. Sie können sehen, dass viel mehr Code benötigt wird, um die gleiche Aufgabe auszuführen. Aber der Vorteil des zusätzlichen Codes reduziert die Zeit um fast 3/4 und ermöglicht es dem Befehl, in 36 ms gegenüber 148 ms zu starten.

Ausführen von Runspaces: Eine Schritt-für-Schritt-Anleitung
Die Verwendung von Runspaces kann anfangs eine Herausforderung sein, da es keine PowerShell-Befehlsunterstützung mehr gibt. Sie müssen direkt mit .NET-Klassen arbeiten. In diesem Abschnitt werden wir analysieren, was erforderlich ist, um in PowerShell eine Runspace zu erstellen.
In dieser Schritt-für-Schritt-Anleitung erstellen Sie einen separaten Runspace von Ihrer PowerShell-Konsole und eine separate PowerShell-Instanz. Dann weisen Sie dem neuen Runspace die neue PowerShell-Instanz zu und fügen Code zu dieser Instanz hinzu.
Erstellen des Runspace
Das Erste, was Sie tun müssen, ist, Ihren neuen Runspace zu erstellen. Dies erfolgt mithilfe der Klasse „RunspaceFactory“. Speichern Sie dies in einer Variablen, wie im folgenden Beispiel gezeigt, damit später darauf verwiesen werden kann.
Nachdem der Runspace erstellt wurde, weisen Sie ihn einer PowerShell-Instanz zu, um PowerShell-Code auszuführen. Hierfür verwenden Sie die Klasse „PowerShell“ und speichern sie ähnlich wie den Runspace in einer Variablen, wie im folgenden Beispiel gezeigt.
Fügen Sie als Nächstes den Runspace Ihrer PowerShell-Instanz hinzu, öffnen Sie den Runspace, um Code ausführen zu können, und fügen Sie Ihren Skriptblock hinzu. Dies wird unten mit einem Skriptblock zum Warten von fünf Sekunden gezeigt.
Ausführen des Runspaces
Bisher wurde der Skriptblock noch nicht ausgeführt. Bisher wurde lediglich alles für den Runspace definiert. Um den Skriptblock auszuführen, gibt es zwei Möglichkeiten.
- Invoke() – Die Methode
Invoke()
führt den Skriptblock im Runspace aus, wartet jedoch darauf, zur Konsole zurückzukehren, bis der Runspace zurückkehrt. Dies ist nützlich, um zu überprüfen, ob Ihr Code ordnungsgemäß ausgeführt wird, bevor Sie ihn freigeben. - BeginInvoke() – Die Verwendung der Methode
BeginInvoke()
sorgt tatsächlich für eine Leistungssteigerung. Dadurch wird der Skriptblock im Runspace gestartet und Sie kehren sofort zur Konsole zurück.
Wenn Sie BeginInvoke()
verwenden, speichern Sie die Ausgabe in einer Variablen, da diese für den Status des Skriptblocks im Runspace erforderlich ist, wie unten gezeigt.
Sobald Sie die Ausgabe von BeginInvoke()
in einer Variablen gespeichert haben, können Sie diese überprüfen, um den Status des Jobs anhand der Eigenschaft IsCompleted
zu sehen, wie unten gezeigt.

Ein weiterer Grund, die Ausgabe in einer Variablen zu speichern, ist, dass im Gegensatz zur Methode Invoke()
die Methode BeginInvoke()
das Ergebnis nicht automatisch zurückgibt, wenn der Code fertig ist. Um dies zu tun, müssen Sie die Methode EndInvoke()
verwenden, sobald sie abgeschlossen ist.
In diesem Beispiel gibt es keine Ausgabe, aber um den Aufruf zu beenden, verwenden Sie den folgenden Befehl.
Sobald alle Aufgaben, die Sie in der Runspace-Warteschlange eingereiht haben, beendet sind, sollten Sie die Runspace immer schließen. Dadurch kann der automatische Garbage Collection-Prozess von PowerShell ungenutzte Ressourcen bereinigen. Unten finden Sie den Befehl, den Sie dafür verwenden würden.
Verwendung von Runspace-Pools
Die Verwendung eines Runspace verbessert zwar die Leistung, stößt jedoch auf eine wesentliche Einschränkung eines einzelnen Threads. Hier kommen Runspace-Pools ins Spiel, da sie mehrere Threads verwenden können.
In der vorherigen Sektion haben Sie nur mit zwei Runspaces gearbeitet. Einen für die PowerShell-Konsole selbst und einen, den Sie manuell erstellt haben. Runspace-Pools ermöglichen es Ihnen, mehrere Runspaces im Hintergrund mit einer einzigen Variablen zu verwalten.
Obwohl dieses Verhalten mit mehreren Runspace-Objekten erreicht werden kann, erleichtert die Verwendung eines Runspace-Pools die Verwaltung erheblich.
Runspace-Pools unterscheiden sich von einzelnen Runspaces in ihrer Konfiguration. Ein wesentlicher Unterschied besteht darin, dass Sie die maximale Anzahl von Threads festlegen, die für den Runspace-Pool verwendet werden können. Bei einem einzelnen Runspace ist dies auf einen Thread beschränkt, aber mit einem Pool können Sie die maximale Anzahl von Threads festlegen, die der Pool erreichen kann.
Die empfohlene Anzahl von Threads in einem Runspace-Pool hängt von der Anzahl der durchgeführten Aufgaben und der Leistungsfähigkeit der Maschine ab, auf der der Code ausgeführt wird. Das Erhöhen der maximalen Anzahl von Threads beeinträchtigt in den meisten Fällen nicht die Geschwindigkeit, es kann jedoch auch keinen Vorteil bringen.
Geschwindigkeitsdemonstration des Runspace-Pools
Um ein Beispiel zu zeigen, wo ein Runspace-Pool einem einzelnen Runspace überlegen ist, möchten Sie vielleicht zehn neue Dateien erstellen. Wenn Sie einen einzelnen Runspace für diese Aufgabe verwenden würden, würden Sie die erste Datei erstellen, dann zur zweiten wechseln und dann zur dritten usw., bis alle zehn erstellt wurden. Der Scriptblock für dieses Beispiel könnte wie folgt aussehen. Sie würden diesem Scriptblock in einer Schleife zehn Dateinamen übergeben und sie alle würden erstellt werden.
In dem folgenden Beispiel wird ein Scriptblock definiert, der ein kurzes Skript enthält, das einen Namen akzeptiert und eine Datei mit diesem Namen erstellt. Ein Runspace-Pool wird mit maximal 5 Threads erstellt.
Dann wird eine Schleife zehnmal durchlaufen und jedes Mal wird die Nummer der Iteration an $_
zugewiesen. Es hätte also 1 bei der ersten Iteration, 2 bei der zweiten usw.
Die Schleife erstellt ein PowerShell-Objekt, weist den Scriptblock und das Argument für das Skript zu und startet den Vorgang.
Schließlich wartet die Schleife am Ende darauf, dass alle Warteschlangenaufgaben abgeschlossen sind.
Statt Threads einzeln zu erstellen, werden jetzt fünf gleichzeitig erstellt. Ohne Runspace-Pools müssten Sie fünf separate Runspaces und fünf separate Powershell
-Instanzen erstellen und verwalten. Diese Verwaltung wird schnell unübersichtlich.
Stattdessen können Sie einen Runspace-Pool erstellen, eine PowerShell-Instanz verwenden, denselben Codeblock und dieselbe Schleife verwenden. Der Unterschied besteht darin, dass der Runspace automatisch alle fünf Threads verwendet.
Erstellen von Runspace-Pools
Die Erstellung eines Runspace-Pools ist sehr ähnlich wie der Runspace, der in einem vorherigen Abschnitt erstellt wurde. Im Folgenden finden Sie ein Beispiel, wie es gemacht wird. Die Hinzufügung eines Scriptblocks und der Invoke-Prozess ist identisch mit einem Runspace. Wie Sie unten sehen können, wird der Runspace-Pool mit maximal fünf Threads erstellt.
Vergleich von Runspaces und Runspace-Pools hinsichtlich Geschwindigkeit
Um den Unterschied zwischen einem Runspace und einem Runspace-Pool zu zeigen, erstellen Sie einen Runspace und führen Sie den Befehl Start-Sleep
aus dem vorherigen Beispiel aus. Diesmal muss er jedoch 10 Mal ausgeführt werden. Wie Sie im untenstehenden Code sehen können, wird ein Runspace erstellt, der 5 Sekunden lang pausiert.
Beachten Sie, dass Sie aufgrund der Verwendung eines einzelnen Runspaces warten müssen, bis er abgeschlossen ist, bevor ein weiterer Invoke gestartet werden kann. Aus diesem Grund wurde eine 100ms-Pause hinzugefügt, bis der Job abgeschlossen ist. Obwohl dies reduziert werden kann, werden Sie abnehmende Ergebnisse feststellen, da Sie mehr Zeit damit verbringen, zu überprüfen, ob der Job abgeschlossen ist, als auf das Ende des Jobs zu warten.
Aus dem untenstehenden Beispiel können Sie sehen, dass es etwa 51 Sekunden dauerte, um 10 Sätze von 5-sekündigen Pausen abzuschließen.

Nun wechseln Sie von einem einzelnen Runspace zu einem Runspace-Pool. Im Folgenden finden Sie den auszuführenden Code. Sie können sehen, dass es einige Unterschiede in der Verwendung der beiden im untenstehenden Code bei Verwendung eines Runspace-Pools gibt.
Wie Sie unten sehen können, dauert dies etwas über 10 Sekunden, was im Vergleich zu den 51 Sekunden für den einzelnen Runspace erheblich verbessert ist.

Im Folgenden finden Sie eine Zusammenfassung der Unterschiede zwischen einem Runspace und einem Runspace-Pool in diesen Beispielen.
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 |
Langsam in Runspaces mit PoshRSJob einsteigen
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.
Dasselbe passiert mit PowerShell, wo einige Leute PSJobs anstelle von Runspaces verwenden, wegen der Benutzerfreundlichkeit. Es gibt ein paar Dinge, die getan werden können, um den Unterschied zu teilen und eine bessere Leistung zu erzielen, ohne die Verwendung zu komplizieren.
Es gibt ein weit verbreitetes Modul namens PoshRSJob, das Module enthält, die dem Stil von normalen PSJobs entsprechen, aber den zusätzlichen Vorteil der Verwendung von Runspaces bieten. Anstatt den gesamten Code zum Erstellen des Runspaces und des PowerShell-Objekts angeben zu müssen, erledigt das PoshRSJob-Modul all das, wenn Sie die Befehle ausführen.
Um das Modul zu installieren, führen Sie den folgenden Befehl in einer administrativen PowerShell-Sitzung aus.
Sobald das Modul installiert ist, können Sie sehen, dass die Befehle die gleichen wie die PSJob-Befehle sind, jedoch mit einem RS-Präfix. Anstatt Start-Job
heißt es Start-RSJob
. Anstatt Get-Job
heißt es Get-RSJob
.
Nachfolgend finden Sie ein Beispiel, wie der gleiche Befehl in einem PSJob und dann erneut in einem RSJob ausgeführt wird. Wie Sie sehen können, haben sie eine sehr ähnliche Syntax und Ausgabe, sind aber nicht ganz identisch.

Nachfolgend finden Sie Code, der verwendet werden kann, um den Unterschied in der Geschwindigkeit zwischen einem PSJob und einem RSJob zu vergleichen.
Wie Sie unten sehen können, gibt es einen großen Geschwindigkeitsunterschied, da die RSJobs immer noch Runspaces unter der Oberfläche verwenden.

Foreach-Object -Parallel
Die PowerShell-Community hat sich eine einfachere und integrierte Möglichkeit gewünscht, Prozesse schnell in mehreren Threads auszuführen. Der Parallel-Schalter ist das Ergebnis davon.
Zum Zeitpunkt des Verfassens dieses Textes befand sich PowerShell 7 noch in der Vorschau, aber sie haben einen Parallel
-Parameter zum Foreach-Object
-Befehl hinzugefügt. Bei diesem Prozess werden Runspaces verwendet, um den Code zu parallelisieren, und der Scriptblock, der für Foreach-Object
verwendet wird, wird als Scriptblock für den Runspace verwendet.
Obwohl die Details noch ausgearbeitet werden, könnte dies in Zukunft eine einfachere Möglichkeit sein, Runspaces zu verwenden. Wie Sie unten sehen können, können Sie schnell durch viele Sätze von Pausen iterieren.

Herausforderungen bei der Multithreading
Obwohl Multithreading bisher nur positiv klang, ist dies nicht ganz der Fall. Es gibt viele Herausforderungen, die mit der Multithreading jeder Codezeile einhergehen.
Verwendung von Variablen
Eine der größten und offensichtlichsten Herausforderungen bei der Multithreading besteht darin, dass Sie Variablen nicht ohne Weiteres teilen können, es sei denn, Sie geben sie als Argumente weiter. Eine Ausnahme bildet eine synchronisierte Hashtable, aber das ist ein Thema für einen anderen Tag.
Sowohl PSJobs als auch Runspaces haben keinen Zugriff auf vorhandene Variablen und es gibt keine Möglichkeit, mit Variablen in verschiedenen Runspaces von Ihrer Konsole aus zu interagieren.
Dies stellt eine große Herausforderung dar, wenn es darum geht, Informationen dynamisch an diese Jobs zu übergeben. Die Antwort ist je nach Art der verwendeten Multithreading-Technik unterschiedlich.
Für Start-Job
und Start-RSJob
aus dem PoshRSJob-Modul können Sie den Parameter ArgumentList
verwenden, um eine Liste von Objekten bereitzustellen, die als Parameter an das Scriptblock in der von Ihnen angegebenen Reihenfolge übergeben werden. Unten finden Sie Beispiele für die Befehle, die für PSJobs und RSJobs verwendet werden.
PSJob:
RSJob:
Native Runspaces bieten nicht die gleiche Benutzerfreundlichkeit. Stattdessen müssen Sie die AddArgument()
-Methode auf dem PowerShell-Objekt verwenden. Unten finden Sie ein Beispiel, wie es für jeden aussehen würde.
Runspace:
Während Runspace-Pools gleich funktionieren, ist unten ein Beispiel, wie Sie einem Runspace-Pool ein Argument hinzufügen können.
Protokollierung
Mehrfädige Verarbeitung bringt auch Herausforderungen bei der Protokollierung mit sich. Da jeder Thread unabhängig voneinander arbeitet, können sie nicht alle an denselben Ort protokollieren. Wenn Sie beispielsweise mit mehreren Threads in eine Datei protokollieren würden, könnte jeder Thread nur schreiben, wenn kein anderer Thread schreibt. Dies kann Ihren Code verlangsamen oder ihn ganz zum Absturz bringen.
Als Beispiel finden Sie unten Code, der versucht, 100 Mal in eine einzelne Datei zu protokollieren, wobei 5 Threads in einem Runspace-Pool verwendet werden.
Aus der Ausgabe sehen Sie keine Fehler, aber wenn Sie die Größe der Textdatei überprüfen, können Sie unten sehen, dass nicht alle 100 Jobs korrekt abgeschlossen wurden.

Eine Möglichkeit, dies zu umgehen, besteht darin, in separate Dateien zu protokollieren. Dadurch wird das Problem der Dateispeicherung vermieden, aber dann haben Sie viele Protokolldateien, die Sie durchsuchen müssten, um herauszufinden, was alles passiert ist.
Eine weitere Alternative besteht darin, dass Sie die Zeitplanung einiger Ausgaben zulassen und nur protokollieren, was ein Job getan hat, sobald er abgeschlossen ist. Dadurch können Sie alles über Ihre ursprüngliche Sitzung seriell abwickeln, aber Sie verlieren einige Details, da Sie nicht unbedingt wissen, in welcher Reihenfolge alles aufgetreten ist.
Zusammenfassung
Obwohl Multithreading enorme Leistungssteigerungen bieten kann, kann es auch Kopfschmerzen verursachen. Während einige Arbeitslasten stark profitieren können, profitieren andere überhaupt nicht. Es gibt viele Vor- und Nachteile bei der Verwendung von Multithreading, aber wenn es richtig eingesetzt wird, können Sie die Laufzeit Ihres Codes drastisch reduzieren.
Weitere Informationen
Source:
https://adamtheautomator.com/powershell-multithreading/