Kann Powershell Befehle parallel ausführen?

125

Ich habe ein Powershell-Skript, um eine Stapelverarbeitung für eine Reihe von Bildern durchzuführen, und ich möchte eine parallele Verarbeitung durchführen. Powershell scheint einige Hintergrundverarbeitungsoptionen wie Startjob, Wartejob usw. zu haben, aber die einzige gute Ressource, die ich für die parallele Arbeit gefunden habe, war das Schreiben des Textes eines Skripts und das Ausführen dieser ( PowerShell Multithreading ).

Im Idealfall möchte ich etwas Ähnliches wie parallele foreach in .net 4.

Etwas ziemlich Scheinloses wie:

foreach-parallel -threads 4 ($file in (Get-ChildItem $dir))
{
   .. Do Work
}

Vielleicht wäre ich besser dran, wenn ich einfach auf c # fallen würde ...

Alan Jackson
quelle
tl; dr: receive-job (wait-job ($a = start-job { "heyo!" })); remove-job $a oder $a = start-job { "heyo!" }; wait-job $a; receive-job $a; remove-job $aBeachten Sie auch, dass receive-jobSie möglicherweise gar nichts bekommen , wenn Sie anrufen, bevor der Job beendet ist.
Andrew
Auch(get-job $a).jobstateinfo.state;
Andrew

Antworten:

99

Sie können parallele Jobs in Powershell 2 mithilfe von Hintergrundjobs ausführen . Schauen Sie sich Start-Job und die anderen Job-Cmdlets an.

# Loop through the server list
Get-Content "ServerList.txt" | %{

  # Define what each job does
  $ScriptBlock = {
    param($pipelinePassIn) 
    Test-Path "\\$pipelinePassIn\c`$\Something"
    Start-Sleep 60
  }

  # Execute the jobs in parallel
  Start-Job $ScriptBlock -ArgumentList $_
}

Get-Job

# Wait for it all to complete
While (Get-Job -State "Running")
{
  Start-Sleep 10
}

# Getting the information back from the jobs
Get-Job | Receive-Job
Steve Townsend
quelle
3
Also habe ich diesen Vorschlag mehrmals versucht, aber es scheint, dass meine Variablen nicht richtig erweitert werden. Um dasselbe Beispiel zu verwenden, wenn diese Zeile ausgeführt wird: Test-Path "\\$_\c$\Something"Ich würde erwarten, dass sie $_in das aktuelle Element erweitert wird. Dies ist jedoch nicht der Fall. Stattdessen wird ein leerer Wert zurückgegeben. Dies scheint nur innerhalb von Skriptblöcken zu geschehen. Wenn ich diesen Wert unmittelbar nach dem ersten Kommentar schreibe, scheint er korrekt zu funktionieren.
rjg
1
@likwid - klingt wie eine separate Frage für die Website
Steve Townsend
Wie kann ich die Ausgabe des Jobs anzeigen, der im Hintergrund ausgeführt wird?
SimpleGuy
@SimpleGuy - Informationen zur Ausgabeerfassung finden Sie hier - stackoverflow.com/questions/15605095/… - Sie können dies anscheinend nicht zuverlässig anzeigen, bis der Hintergrundjob abgeschlossen ist.
Steve Townsend
@SteveTownsend Danke! Tatsächlich ist die Anzeige der Ausgabe auf dem Bildschirm nicht so gut. Kommt mit Verzögerung, also nicht nützlich für mich. Stattdessen habe ich einen Prozess auf einem neuen Terminal (Shell) gestartet, sodass jetzt jeder Prozess auf einem anderen Terminal ausgeführt wird, wodurch die Ansicht des Fortschritts viel besser und sauberer wird.
SimpleGuy
98

Die Antwort von Steve Townsend ist theoretisch richtig, aber nicht in der Praxis, wie @likwid hervorhob. Mein überarbeiteter Code berücksichtigt die Job-Kontext-Barriere - nichts überschreitet diese Barriere standardmäßig! Die automatische $_Variable kann somit in der Schleife verwendet werden, kann jedoch nicht direkt im Skriptblock verwendet werden, da sie sich in einem separaten Job befindet, der vom Job erstellt wurde.

Um Variablen aus dem übergeordneten Kontext an den untergeordneten Kontext zu übergeben, verwenden Sie den -ArgumentListParameter on Start-Job, um sie zu senden, und verwenden Sie sie paramim Skriptblock, um sie zu empfangen.

cls
# Send in two root directory names, one that exists and one that does not.
# Should then get a "True" and a "False" result out the end.
"temp", "foo" | %{

  $ScriptBlock = {
    # accept the loop variable across the job-context barrier
    param($name) 
    # Show the loop variable has made it through!
    Write-Host "[processing '$name' inside the job]"
    # Execute a command
    Test-Path "\$name"
    # Just wait for a bit...
    Start-Sleep 5
  }

  # Show the loop variable here is correct
  Write-Host "processing $_..."

  # pass the loop variable across the job-context barrier
  Start-Job $ScriptBlock -ArgumentList $_
}

# Wait for all to complete
While (Get-Job -State "Running") { Start-Sleep 2 }

# Display output from all jobs
Get-Job | Receive-Job

# Cleanup
Remove-Job *

(Im Allgemeinen möchte ich einen Verweis auf die PowerShell-Dokumentation als unterstützenden Beweis geben, aber leider war meine Suche erfolglos. Wenn Sie zufällig wissen, wo die Kontexttrennung dokumentiert ist, geben Sie hier einen Kommentar ab, um mich zu informieren!)

Michael Sorens
quelle
Danke für diese Antwort. Ich habe versucht, Ihre Lösung zu verwenden, konnte sie jedoch nicht vollständig zum Laufen bringen. Können Sie sich meine Frage hier ansehen : stackoverflow.com/questions/28509659/…
David sagt Reinstate Monica
Alternativ ist es ziemlich einfach, eine separate Skriptdatei aufzurufen. Benutz einfachStart-Job -FilePath script.ps1 -ArgumentList $_
Chad Zawistowski
Ein alternativer Ansatz besteht darin, einen vorläufigen Durchlauf der Skripterstellung durchzuführen, bei dem nur eine variable Erweiterung durchgeführt wird, und dann die generierten Skripte parallel aufzurufen. Ich habe ein kleines Tool, das möglicherweise an die Skripterstellung angepasst ist, obwohl es nie zur Unterstützung der Skripterstellung gedacht war. Sie können es hier sehen .
Walter Mitty
Das funktioniert. Ich kann jedoch keinen Live-Feed-Ausgabestream von ScriptBlock erhalten. Die Ausgabe wird nur gedruckt, wenn ScriptBlock zurückkehrt.
Vothaison
8

http://gallery.technet.microsoft.com/scriptcenter/Invoke-Async-Allows-you-to-83b0c9f0

Ich habe einen invoke-async erstellt, mit dem Sie mehrere Skriptblöcke / Cmdlets / Funktionen gleichzeitig ausführen können. Dies ist ideal für kleine Jobs (Subnetz-Scan oder WMI-Abfrage für Hunderte von Computern), da der Aufwand für die Erstellung eines Runspace im Vergleich zur Startzeit des Startjobs ziemlich drastisch ist. Es kann so verwendet werden.

mit scriptblock,

$sb = [scriptblock] {param($system) gwmi win32_operatingsystem -ComputerName $system | select csname,caption} 

$servers = Get-Content servers.txt 

$rtn = Invoke-Async -Set $server -SetParam system  -ScriptBlock $sb

nur Cmdlet / Funktion

$servers = Get-Content servers.txt 

$rtn = Invoke-Async -Set $servers -SetParam computername -Params @{count=1} -Cmdlet Test-Connection -ThreadCount 50
jrich523
quelle
8

Es gibt heutzutage so viele Antworten darauf:

  1. Jobs (oder Threadjobs in PS 6/7 oder im Modul)
  2. Startvorgang
  3. Workflows
  4. Powershell API mit einem anderen Runspace
  5. Aufrufbefehl mit mehreren Computern, die alle localhost sein können (müssen admin sein)
  6. Mehrere Sitzungsregisterkarten (Runspace) in der ISE oder Remote-Powershell-ISE-Registerkarten
  7. Powershell 7 hat eine foreach-object -parallelAlternative für # 4

Hier sind Workflows mit buchstäblich einer foreach-Parallele:

workflow work {
  foreach -parallel ($i in 1..3) { 
    sleep 5 
    "$i done" 
  }
}

work

3 done
1 done
2 done

Oder ein Workflow mit einem parallelen Block:

function sleepfor($time) { sleep $time; "sleepfor $time done"}

workflow work {
  parallel {
    sleepfor 3
    sleepfor 2
    sleepfor 1
  }
  'hi'
}

work 

sleepfor 1 done
sleepfor 2 done
sleepfor 3 done
hi

Hier ist eine API mit Runspaces-Beispiel:

$a =  [PowerShell]::Create().AddScript{sleep 5;'a done'}
$b =  [PowerShell]::Create().AddScript{sleep 5;'b done'}
$c =  [PowerShell]::Create().AddScript{sleep 5;'c done'}
$r1,$r2,$r3 = ($a,$b,$c).begininvoke() # run in background
$a.EndInvoke($r1); $b.EndInvoke($r2); $c.EndInvoke($r3) # wait
($a,$b,$c).streams.error # check for errors
($a,$b,$c).dispose() # clean

a done
b done
c done
js2010
quelle
7

Die Einrichtung von Hintergrundjobs ist teuer und nicht wiederverwendbar. PowerShell MVP Oisin Grehan hat ein gutes Beispiel für PowerShell-Multithreading.

(Die Website vom 25.10.2010 ist nicht verfügbar, aber über das Webarchiv zugänglich.)

Ich habe hier ein angepasstes Oisin-Skript zur Verwendung in einer Datenladeroutine verwendet:

http://rsdd.codeplex.com/SourceControl/changeset/view/a6cd657ea2be#Invoke-RSDDThreaded.ps1

Chad Miller
quelle
Link Rot hat für diese Antwort eingesetzt
Luke
4

Um frühere Antworten zu vervollständigen, können Sie auch Wait-Jobwarten, bis alle Jobs abgeschlossen sind:

For ($i=1; $i -le 3; $i++) {
    $ScriptBlock = {
        Param (
            [string] [Parameter(Mandatory=$true)] $increment
        )

        Write-Host $increment
    }

    Start-Job $ScriptBlock -ArgumentList $i
}

Get-Job | Wait-Job | Receive-Job
Thomas
quelle
0

In Powershell 7 können Sie ForEach-Object -Parallel verwenden

$Message = "Output:"
Get-ChildItem $dir | ForEach-Object -Parallel {
    "$using:Message $_"
} -ThrottleLimit 4
Izharsa
quelle
0

Wenn Sie die neueste plattformübergreifende Powershell (die Sie übrigens verwenden sollten) https://github.com/powershell/powershell#get-powershell verwenden , können Sie einzelne hinzufügen &, um parallele Skripts auszuführen. (Verwenden; Zum sequentiellen Ausführen verwenden)

In meinem Fall musste ich 2 npm-Skripte parallel ausführen: npm run hotReload & npm run dev


Sie können npm auch powershellfür seine Skripte einrichten (standardmäßig cmdunter Windows).

Führen Sie den Projektstammordner aus: npm config set script-shell pwsh --userconfig ./.npmrc und verwenden Sie dann den einzelnen npm-Skriptbefehl:npm run start

"start":"npm run hotReload & npm run dev"
JerryGoyal
quelle