Czy program PowerShell może uruchamiać polecenia równolegle?

125

Mam skrypt PowerShell do przetwarzania wsadowego na kilku obrazach i chciałbym przeprowadzić równoległe przetwarzanie. Wydaje się, że Powershell ma pewne opcje przetwarzania w tle, takie jak start-job, wait-job itp., Ale jedynym dobrym źródłem, które znalazłem do wykonywania równoległej pracy, było napisanie tekstu skryptu i uruchomienie go ( wielowątkowość PowerShell )

Idealnie, chciałbym coś podobnego do równoległego foreach w .net 4.

Coś całkiem pozornego, jak:

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

Może lepiej byłoby, gdybym zszedł do C # ...

Alan Jackson
źródło
tl; dr: receive-job (wait-job ($a = start-job { "heyo!" })); remove-job $a lub $a = start-job { "heyo!" }; wait-job $a; receive-job $a; remove-job $aZauważ również, że jeśli zadzwonisz receive-jobprzed zakończeniem zadania, możesz w ogóle nic nie otrzymać.
Andrew,
Również(get-job $a).jobstateinfo.state;
Andrew

Odpowiedzi:

99

Możesz wykonywać równoległe zadania w programie PowerShell 2 przy użyciu zadań w tle . Sprawdź Start-Job i inne polecenia cmdlet Job.

# 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
źródło
3
Próbowałem więc kilka razy tej sugestii, ale wydaje się, że moje zmienne nie są poprawnie rozwijane. Aby użyć tego samego przykładu, kiedy ta linia jest wykonywana: Test-Path "\\$_\c$\Something"spodziewałbym się, że rozszerzy $_się do bieżącej pozycji. Jednak tak nie jest. Zamiast tego zwraca pustą wartość. Wydaje się, że dzieje się to tylko z poziomu bloków skryptu. Jeśli wypiszę tę wartość natychmiast po pierwszym komentarzu, wydaje się, że działa poprawnie.
rjg
1
@likwid - brzmi jak osobne pytanie do strony
Steve Townsend
Jak mogę wyświetlić dane wyjściowe zadania, które jest uruchomione w tle?
SimpleGuy
@SimpleGuy - zobacz tutaj, aby uzyskać informacje na temat przechwytywania danych wyjściowych - stackoverflow.com/questions/15605095/… - wydaje się, że nie możesz wyświetlić tego rzetelnie, dopóki zadanie w tle nie zostanie zakończone.
Steve Townsend
@SteveTownsend Thanks! Właściwie oglądanie wyjścia nie jest tak dobre na ekranie. Przychodzi z opóźnieniem, więc nie jest dla mnie przydatny. Zamiast tego rozpocząłem proces na nowym terminalu (powłoce), więc teraz każdy proces działa na innym terminalu, co daje znacznie lepszy i czystszy widok postępu.
SimpleGuy
98

Odpowiedź Steve'a Townsenda jest poprawna w teorii, ale nie w praktyce, jak zauważył @likwid. Mój poprawiony kod uwzględnia barierę związaną z kontekstem pracy - domyślnie nic nie przekracza tej bariery! $_Zmienna automatyczna może być zatem używana w pętli, ale nie może być używana bezpośrednio w bloku skryptu, ponieważ znajduje się w oddzielnym kontekście utworzonym przez zadanie.

Aby przekazać zmienne z kontekstu nadrzędnego do kontekstu podrzędnego, użyj -ArgumentListparametru on, Start-Jobaby je wysłać, i użyj paramwewnątrz bloku skryptu, aby je odebrać.

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 *

(Generalnie lubię podawać odniesienie do dokumentacji PowerShell jako dowód potwierdzający, ale niestety moje poszukiwania były bezowocne. Jeśli wiesz, gdzie udokumentowano separację kontekstów, opublikuj tutaj komentarz, aby dać mi znać!)

Michael Sorens
źródło
Dzięki za tę odpowiedź. Próbowałem użyć twojego rozwiązania, ale nie mogłem go w pełni uruchomić. Czy możesz spojrzeć na moje pytanie tutaj: stackoverflow.com/questions/28509659/ ...
David mówi Przywróć Monikę
Alternatywnie, całkiem łatwo jest wywołać oddzielny plik skryptu. Po prostu użyjStart-Job -FilePath script.ps1 -ArgumentList $_
Chad Zawistowski
Alternatywnym podejściem jest wykonanie wstępnego przebiegu generowania skryptu, w którym nie robi się nic poza rozszerzaniem zmiennych, a następnie równoległe wywoływanie wygenerowanych skryptów. Mam małe narzędzie, które można by dostosować do generowania skryptów, chociaż nigdy nie było przeznaczone do wspierania generowania skryptów. Możesz to zobaczyć tutaj .
Walter Mitty,
To działa. Ale nie mogę uzyskać strumienia wyjściowego na żywo z ScriptBlock. Dane wyjściowe są drukowane tylko wtedy, gdy zwraca ScriptBlock.
vothaison
8

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

Stworzyłem invoke-async, który umożliwia uruchamianie wielu bloków skryptów / poleceń cmdlet / funkcji w tym samym czasie. jest to świetne rozwiązanie w przypadku małych zadań (skanowanie podsieci lub kwerendy wmi na setkach maszyn), ponieważ obciążenie związane z tworzeniem obszaru roboczego w porównaniu z czasem uruchamiania zadania jest dość drastyczne. Może być używany w ten sposób.

z blokiem skryptów,

$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

po prostu polecenie cmdlet / funkcja

$servers = Get-Content servers.txt 

$rtn = Invoke-Async -Set $servers -SetParam computername -Params @{count=1} -Cmdlet Test-Connection -ThreadCount 50
jrich523
źródło
8

W dzisiejszych czasach jest tak wiele odpowiedzi na to pytanie:

  1. Jobs (lub Threadjobs w PS 6/7 lub module)
  2. proces startu
  3. przepływy pracy
  4. Powershell api z innym obszarem działania
  5. invoke-command z wieloma komputerami, z których wszystkie mogą być hostami lokalnymi (muszą być administratorami)
  6. karty wielu sesji (obszaru działania) w ISE lub zdalne karty PowerShell ISE
  7. Powershell 7 ma foreach-object -parallelalternatywę dla # 4

Oto przepływy pracy z dosłownie paralelą foreach:

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

work

3 done
1 done
2 done

Lub przepływ pracy z równoległym blokiem:

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

Oto przykład interfejsu API z obszarami uruchomieniowymi:

$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
źródło
7

Zadania w tle są drogie w konfiguracji i nie nadają się do ponownego wykorzystania. PowerShell MVP Oisin Grehan ma dobry przykład wielowątkowości programu PowerShell.

(Witryna 25.10.2010 nie działa, ale jest dostępna za pośrednictwem archiwum internetowego).

Użyłem dostosowanego skryptu Oisin do użycia w procedurze ładowania danych tutaj:

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

Chad Miller
źródło
Zgnilizna linków wkroczyła w tę odpowiedź
Luke
4

Aby uzupełnić poprzednie odpowiedzi, możesz także użyć opcji Wait-Jobczekania na zakończenie wszystkich zadań:

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
Tomasz
źródło
0

W programie PowerShell 7 można użyć funkcji ForEach-Object -Parallel

$Message = "Output:"
Get-ChildItem $dir | ForEach-Object -Parallel {
    "$using:Message $_"
} -ThrottleLimit 4
izharsa
źródło
0

Jeśli używasz najnowszej wieloplatformowej powłoki PowerShell (którą powinieneś btw) https://github.com/powershell/powershell#get-powershell , możesz dodać pojedynczy, &aby uruchamiać równoległe skrypty. (Służy ;do uruchamiania sekwencyjnego)

W moim przypadku potrzebowałem uruchomić równolegle 2 skrypty npm: npm run hotReload & npm run dev


Możesz także skonfigurować npm do używania powershelldla swoich skryptów (domyślnie używa cmdw systemie Windows).

Uruchom z folderu głównego projektu: npm config set script-shell pwsh --userconfig ./.npmrc a następnie użyj pojedynczego polecenia skryptu npm:npm run start

"start":"npm run hotReload & npm run dev"
JerryGoyal
źródło