¿Puede Powershell ejecutar comandos en paralelo?

125

Tengo un script de PowerShell para hacer un procesamiento por lotes en un montón de imágenes y me gustaría hacer un procesamiento paralelo. Powershell parece tener algunas opciones de procesamiento en segundo plano, como start-job, wait-job, etc., pero el único recurso bueno que encontré para hacer un trabajo paralelo fue escribir el texto de un script y ejecutarlo ( PowerShell Multithreading )

Idealmente, me gustaría algo similar a foreach paralelo en .net 4.

Algo bastante parecido a:

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

Tal vez estaría mejor simplemente bajando a c # ...

Alan Jackson
fuente
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 $aTenga en cuenta también que si llama receive-jobantes de que finalice el trabajo, es posible que no obtenga nada.
Andrew
También(get-job $a).jobstateinfo.state;
Andrew

Respuestas:

99

Puede ejecutar trabajos paralelos en Powershell 2 utilizando trabajos en segundo plano . Consulte Start-Job y los otros cmdlets de trabajo.

# 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
fuente
3
Así que probé esta sugerencia varias veces, pero parece que mis variables no se están expandiendo correctamente. Para usar el mismo ejemplo, cuando se ejecuta esta línea: Test-Path "\\$_\c$\Something"esperaría que se expanda $_al elemento actual. Sin embargo, no lo hace. En su lugar, devuelve un valor vacío. Esto solo parece suceder desde dentro de los bloques de script. Si escribo ese valor inmediatamente después del primer comentario, parece que funciona correctamente.
rjg
1
@likwid - parece una pregunta separada para el sitio
Steve Townsend
¿Cómo puedo ver la salida del trabajo que se ejecuta en segundo plano?
SimpleGuy
@SimpleGuy - vea aquí para obtener información sobre la captura de salida - stackoverflow.com/questions/15605095/… - no parece que pueda ver esto de manera confiable hasta que se complete el trabajo en segundo plano.
Steve Townsend
@SteveTownsend ¡Gracias! En realidad, ver la salida no es tan buena en la pantalla. Viene con retraso, por lo que no es útil para mí. En cambio, comencé un proceso en una nueva terminal (shell), por lo que ahora cada proceso se ejecuta en una terminal diferente, lo que brinda una visión del progreso mucho mejor y mucho más limpia.
SimpleGuy
98

La respuesta de Steve Townsend es correcta en teoría, pero no en la práctica, como señaló @likwid. Mi código revisado tiene en cuenta la barrera del contexto laboral: ¡ nada cruza esa barrera por defecto! Por lo tanto, la $_variable automática se puede usar en el bucle pero no se puede usar directamente dentro del bloque de script porque está dentro de un contexto separado creado por el trabajo.

Para pasar variables del contexto primario al contexto secundario, use el -ArgumentListparámetro on Start-Jobpara enviarlo y use paramdentro del bloque de script para recibirlo.

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 *

(En general, me gusta proporcionar una referencia a la documentación de PowerShell como evidencia de apoyo, pero, por desgracia, mi búsqueda ha sido infructuosa. Si sabe dónde está documentada la separación de contexto, ¡publique un comentario aquí para avisarme!)

Michael Sorens
fuente
Gracias por esta respuesta Intenté usar su solución, pero no pude lograr que funcionara por completo. ¿Puedes echar un vistazo a mi pregunta aquí: stackoverflow.com/questions/28509659/…
David dice que reinstales a Monica
Alternativamente, es bastante fácil invocar un archivo de script separado. Solo useStart-Job -FilePath script.ps1 -ArgumentList $_
Chad Zawistowski
Un enfoque alternativo es hacer un pase preliminar de generación de script, donde no se está haciendo nada más que la expansión variable, y luego invocar los scripts generados en paralelo. Tengo una pequeña herramienta que podría adaptarse a la generación de scripts, aunque nunca tuvo la intención de admitir la generación de scripts. Puedes verlo aquí .
Walter Mitty
Esto funciona. Pero no puedo obtener la transmisión de salida en vivo de ScriptBlock. La salida solo se imprime cuando vuelve ScriptBlock.
vothaison
8

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

Creé un invoke-async que le permite ejecutar múltiples bloques de script / cmdlets / funciones al mismo tiempo. Esto es ideal para trabajos pequeños (escaneo de subred o consulta wmi en cientos de máquinas) porque la sobrecarga para crear un espacio de ejecución frente al tiempo de inicio del trabajo de inicio es bastante drástica. Se puede usar así.

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 / function

$servers = Get-Content servers.txt 

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

Hay tantas respuestas a esto en estos días:

  1. trabajos (o threadjobs en PS 6/7 o el módulo)
  2. proceso de inicio
  3. flujos de trabajo
  4. API de PowerShell con otro espacio de ejecución
  5. invocar-comando con múltiples computadoras, que pueden ser localeshost (tienen que ser admin)
  6. pestañas de sesión múltiple (espacio de ejecución) en el ISE o pestañas ISE remotas de PowerShell
  7. Powershell 7 tiene una foreach-object -parallelalternativa para # 4

Aquí hay flujos de trabajo con literalmente un foreach-paralelo:

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

work

3 done
1 done
2 done

O un flujo de trabajo con un bloque paralelo:

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

Aquí hay un ejemplo de API con espacios de ejecución:

$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
fuente
4

Para completar las respuestas anteriores, también puede usar Wait-Jobpara esperar a que se completen todos los trabajos:

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
fuente
0

En Powershell 7 puede usar ForEach-Object -Parallel

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

Si está utilizando el último PowerShell multiplataforma (que debería ser por cierto) https://github.com/powershell/powershell#get-powershell , puede agregar un solo &para ejecutar scripts paralelos. (Utilizar; para correr secuencialmente)

En mi caso, necesitaba ejecutar scripts de 2 npm en paralelo: npm run hotReload & npm run dev


También puede configurar npm para usarlo powershellen sus scripts (por defecto lo usa cmden Windows).

Ejecute desde la carpeta raíz del proyecto: npm config set script-shell pwsh --userconfig ./.npmrc y luego use el comando de script npm único:npm run start

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