Powershell peut-il exécuter des commandes en parallèle?

125

J'ai un script PowerShell pour effectuer un traitement par lots sur un tas d'images et j'aimerais faire un traitement parallèle. Powershell semble avoir des options de traitement en arrière-plan telles que start-job, wait-job, etc., mais la seule bonne ressource que j'ai trouvée pour effectuer un travail en parallèle était d'écrire le texte d'un script et de les exécuter ( PowerShell Multithreading )

Idéalement, j'aimerais quelque chose qui ressemble à foreach parallèle dans .net 4.

Quelque chose d'assez insensé comme:

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

Peut-être que je ferais mieux de passer à c # ...

Alan Jackson
la source
tl; dr: receive-job (wait-job ($a = start-job { "heyo!" })); remove-job $a ou $a = start-job { "heyo!" }; wait-job $a; receive-job $a; remove-job $aNotez également que si vous appelez receive-jobavant la fin du travail, vous risquez de ne rien obtenir du tout.
Andrew
Aussi(get-job $a).jobstateinfo.state;
Andrew

Réponses:

99

Vous pouvez exécuter des travaux parallèles dans Powershell 2 à l'aide de travaux d'arrière-plan . Consultez Start-Job et les autres applets de commande de travail.

# 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
la source
3
J'ai donc essayé cette suggestion plusieurs fois, mais il semble que mes variables ne soient pas développées correctement. Pour utiliser le même exemple, lorsque cette ligne s'exécute: Test-Path "\\$_\c$\Something"je m'attendrais à ce qu'elle se développe $_dans l'élément actuel. Cependant, ce n'est pas le cas. Au lieu de cela, il renvoie une valeur vide. Cela ne semble se produire qu'à partir de blocs de script. Si j'écris cette valeur immédiatement après le premier commentaire, cela semble fonctionner correctement.
rjg
1
@likwid - sonne comme une question distincte pour le site
Steve Townsend
Comment puis-je afficher la sortie du travail qui s'exécute en arrière-plan?
SimpleGuy
@SimpleGuy - voir ici pour plus d'informations sur la capture de sortie - stackoverflow.com/questions/15605095/… - ne semble pas que vous puissiez voir cela de manière fiable tant que le travail en arrière-plan n'est pas terminé.
Steve Townsend
@SteveTownsend Merci! En fait, la visualisation de la sortie n'est pas si bonne à l'écran. Livré avec du retard, donc pas utile pour moi. Au lieu de cela, j'ai commencé un processus sur un nouveau terminal (shell), donc maintenant chaque processus s'exécute sur un terminal différent, ce qui donne une vue des progrès beaucoup mieux et beaucoup plus propre.
SimpleGuy
98

La réponse de Steve Townsend est correcte en théorie mais pas en pratique comme l'a souligné @likwid. Mon code révisé prend en compte la barrière du contexte de travail - rien ne franchit cette barrière par défaut! La $_variable automatique peut donc être utilisée dans la boucle mais ne peut pas être utilisée directement dans le bloc de script car elle se trouve dans un contexte distinct créé par le travail.

Pour transmettre des variables du contexte parent au contexte enfant, utilisez le -ArgumentListparamètre on Start-Jobpour l'envoyer et utilisez paramà l'intérieur du bloc de script pour le recevoir.

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 *

(J'aime généralement fournir une référence à la documentation PowerShell comme preuve à l'appui mais, hélas, ma recherche a été infructueuse. Si vous savez où la séparation de contexte est documentée, postez un commentaire ici pour me le faire savoir!)

Michael Sorens
la source
Merci pour cette réponse. J'ai essayé d'utiliser votre solution, mais je n'ai pas pu la faire fonctionner pleinement. Pouvez-vous jeter un oeil à ma question ici: stackoverflow.com/questions/28509659/…
David dit Réintégrer Monica le
Alternativement, il est assez facile d'appeler un fichier de script séparé. Just useStart-Job -FilePath script.ps1 -ArgumentList $_
Chad Zawistowski
Une autre approche consiste à effectuer une passe préliminaire de génération de script, où rien n'est fait d'autre que l'expansion de variables, puis à appeler les scripts générés en parallèle. J'ai un petit outil qui pourrait être adapté à la génération de scripts, bien qu'il n'ait jamais été conçu pour supporter la génération de scripts. Vous pouvez le voir ici .
Walter Mitty
Cela marche. Mais je ne peux pas obtenir le flux de sortie du flux en direct de ScriptBlock. La sortie n'est imprimée que lorsque ScriptBlock revient.
vothaison le
8

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

J'ai créé un invoke-async qui vous permet d'exécuter plusieurs blocs de script / cmdlets / fonctions en même temps. c'est idéal pour les petits travaux (scan de sous-réseau ou requête wmi contre des centaines de machines) car la surcharge de création d'un espace d'exécution par rapport au temps de démarrage du travail de démarrage est assez drastique. Il peut être utilisé comme ça.

avec 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

juste cmdlet / fonction

$servers = Get-Content servers.txt 

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

Il y a tellement de réponses à cela ces jours-ci:

  1. jobs (ou threadjobs dans PS 6/7 ou le module)
  2. processus de démarrage
  3. workflows
  4. API PowerShell avec un autre espace d'exécution
  5. invoke-command avec plusieurs ordinateurs, qui peuvent tous être localhost (doivent être admin)
  6. onglets multiples de session (espace d'exécution) dans l'ISE, ou onglets à distance PowerShell ISE
  7. Powershell 7 a foreach-object -parallelune alternative pour # 4

Voici les flux de travail avec littéralement un foreach -parallel:

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

work

3 done
1 done
2 done

Ou un workflow avec un bloc parallèle:

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

Voici un exemple d'API avec des espaces d'exécution:

$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
la source
7

Les travaux d'arrière-plan sont coûteux à configurer et ne sont pas réutilisables. PowerShell MVP Oisin Grehan a un bon exemple de multi-threading PowerShell.

(Le site du 25/10/2010 est en panne, mais accessible via l'archive Web).

J'ai utilisé un script Oisin adapté pour une utilisation dans une routine de chargement de données ici:

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

Chad Miller
la source
Link rot s'est installé pour cette réponse
Luke
4

Pour compléter les réponses précédentes, vous pouvez également utiliser Wait-Jobpour attendre que toutes les tâches soient terminées:

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
la source
0

Dans Powershell 7, vous pouvez utiliser ForEach-Object -Parallel

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

Si vous utilisez le dernier PowerShell multiplateforme (que vous devriez d'ailleurs) https://github.com/powershell/powershell#get-powershell , vous pouvez en ajouter un &pour exécuter des scripts parallèles. (Utilisez ;pour exécuter séquentiellement)

Dans mon cas, j'avais besoin d'exécuter 2 scripts npm en parallèle: npm run hotReload & npm run dev


Vous pouvez également configurer npm à utiliser powershellpour ses scripts (par défaut, il l'utilise cmdsous Windows).

Exécutez à partir du dossier racine du projet: npm config set script-shell pwsh --userconfig ./.npmrc puis utilisez une seule commande de script npm:npm run start

"start":"npm run hotReload & npm run dev"
JerryGoyal
la source