Powershell può eseguire comandi in parallelo?


125

Ho uno script PowerShell per eseguire un'elaborazione batch su un gruppo di immagini e mi piacerebbe eseguire un'elaborazione parallela. Powershell sembra avere alcune opzioni di elaborazione in background come start-job, wait-job, ecc., Ma l'unica buona risorsa che ho trovato per fare lavoro parallelo è stata la scrittura del testo di uno script e l'esecuzione di quelli ( PowerShell Multithreading )

Idealmente, vorrei qualcosa di simile a parallel foreach in .net 4.

Qualcosa di abbastanza insignificante come:

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

Forse farei meglio a scendere in c # ...


tl; dr: receive-job (wait-job ($a = start-job { "heyo!" })); remove-job $a o $a = start-job { "heyo!" }; wait-job $a; receive-job $a; remove-job $aNota anche che se chiami receive-jobprima che il lavoro sia finito, potresti non ottenere nulla.
Andrew

Inoltre(get-job $a).jobstateinfo.state;
Andrew

Risposte:


99

Puoi eseguire lavori paralleli in Powershell 2 utilizzando i lavori in background . Controlla Start-Job e gli altri cmdlet del processo.

# 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

3
Quindi ho provato questo suggerimento più volte, ma sembra che le mie variabili non vengano espanse correttamente. Per usare lo stesso esempio, quando viene eseguita questa riga: Test-Path "\\$_\c$\Something"mi aspetto che si espanda $_nell'elemento corrente. Tuttavia, non è così. Invece restituisce un valore vuoto. Questo sembra accadere solo all'interno dei blocchi di script. Se scrivo quel valore subito dopo il primo commento, sembra funzionare correttamente.
rjg

1
@likwid - suona come una domanda separata per il sito
Steve Townsend

Come posso visualizzare l'output del lavoro in esecuzione in background?
SimpleGuy

@SimpleGuy - vedi qui per informazioni sulla cattura di uscita - stackoverflow.com/questions/15605095/... - non sembra di poter visualizzare questo modo affidabile fino al completamento sfondo di lavoro.
Steve Townsend

@SteveTownsend Grazie! In realtà la visualizzazione dell'output non è così buona sullo schermo. Viene fornito con ritardo, quindi non è utile per me. Invece ho avviato un processo su un nuovo terminale (shell), quindi ora ogni processo è in esecuzione su un terminale diverso, il che offre una visione dei progressi molto migliore e molto più pulita.
SimpleGuy

98

La risposta di Steve Townsend è corretta in teoria ma non in pratica, come ha sottolineato @likwid. Il mio codice rivisto tiene conto della barriera del contesto lavorativo: nulla oltrepassa quella barriera per impostazione predefinita! La $_variabile automatica può quindi essere utilizzata nel ciclo ma non può essere utilizzata direttamente all'interno del blocco di script perché si trova all'interno di un contesto separato creato dal lavoro.

Per passare variabili dal contesto padre al contesto figlio, utilizzare il -ArgumentListparametro on Start-Jobper inviarlo e utilizzare paramall'interno del blocco di script per riceverlo.

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 *

(In genere mi piace fornire un riferimento alla documentazione di PowerShell come prova di supporto ma, ahimè, la mia ricerca è stata infruttuosa. Se ti capita di sapere dove è documentata la separazione del contesto, pubblica un commento qui per farmelo sapere!)


Grazie per questa risposta. Ho provato a utilizzare la tua soluzione, ma non sono riuscito a farlo funzionare completamente. Puoi dare un'occhiata alla mia domanda qui: stackoverflow.com/questions/28509659/…
David dice Reinstate Monica

In alternativa, è abbastanza facile richiamare un file di script separato. Basta usareStart-Job -FilePath script.ps1 -ArgumentList $_
Chad Zawistowski

Un approccio alternativo consiste nell'eseguire un passaggio preliminare della generazione di script, in cui non viene fatto altro che l'espansione delle variabili, quindi richiamare gli script generati in parallelo. Ho un piccolo strumento che potrebbe essere adattato alla generazione di script, sebbene non sia mai stato concepito per supportare la generazione di script. Puoi vederlo qui .
Walter Mitty

Funziona. Ma non riesco a ottenere il flusso di output del feed live da ScriptBlock. L'output viene stampato solo quando ScriptBlock ritorna.
vothaison

8

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

ho creato un invoke-async che ti consente di eseguire più blocchi di script / cmdlet / funzioni contemporaneamente. questo è ottimo per piccoli lavori (scansione di sottorete o query wmi su centinaia di macchine) perché il sovraccarico per la creazione di uno spazio di esecuzione rispetto al tempo di avvio del lavoro è piuttosto drastico. Può essere usato così.

con 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

solo cmdlet / funzione

$servers = Get-Content servers.txt 

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

8

Ci sono così tante risposte a questo in questi giorni:

  1. job (o threadjobs in PS 6/7 o nel modulo)
  2. start-processo
  3. flussi di lavoro
  4. API PowerShell con un altro spazio di esecuzione
  5. invoke-command con più computer, che possono essere tutti localhost (devono essere admin)
  6. più schede di sessione (runspace) in ISE o schede ISE di PowerShell remoto
  7. Powershell 7 ha foreach-object -parallelun'alternativa per # 4

Ecco i flussi di lavoro con letteralmente un foreach -parallel:

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

work

3 done
1 done
2 done

O un flusso di lavoro con un blocco parallelo:

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

Ecco un esempio di API con runspaces:

$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


4

Per completare le risposte precedenti, puoi anche utilizzare Wait-Jobper attendere il completamento di tutti i lavori:

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

0

In Powershell 7 puoi usare ForEach-Object -Parallel

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

0

Se stai utilizzando il più recente PowerShell multipiattaforma (che dovresti tra l'altro) https://github.com/powershell/powershell#get-powershell , puoi aggiungere un singolo &per eseguire script paralleli. (Utilizzare ;per eseguire in sequenza)

Nel mio caso avevo bisogno di eseguire 2 script npm in parallelo: npm run hotReload & npm run dev


Puoi anche impostare npm da utilizzare powershellper i suoi script (per impostazione predefinita utilizza cmdsu Windows).

Esegui dalla cartella principale del progetto: npm config set script-shell pwsh --userconfig ./.npmrc quindi utilizza un singolo comando script npm:npm run start

"start":"npm run hotReload & npm run dev"
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.