Multihilado en PowerShell: Una Inmersión Profunda

En algún momento, la mayoría de las personas se encontrarán con un problema en el que un script básico de PowerShell es demasiado lento para resolverlo. Esto podría ser la recopilación de datos de muchas computadoras en su red o tal vez la creación de una gran cantidad de usuarios nuevos en Active Directory de una sola vez. Estos son excelentes ejemplos de dónde usar más potencia de procesamiento haría que su código se ejecutara más rápido. ¡Veamos cómo resolver esto usando el multihilo de PowerShell!

La sesión predeterminada de PowerShell es de un solo hilo. Ejecuta un comando y cuando termina, pasa al siguiente comando. Esto es bueno ya que mantiene todo repetible y no utiliza muchos recursos. Pero ¿qué pasa si las acciones que está realizando no dependen una de la otra y tiene recursos de CPU para gastar? En ese caso, es hora de empezar a pensar en el multihilo.

En este artículo, aprenderás cómo entender y usar diversas técnicas de multihilo de PowerShell para procesar múltiples flujos de datos al mismo tiempo pero gestionados a través de la misma consola.

Entendiendo el Multihilo de PowerShell

El multihilo es una forma de ejecutar más de un comando a la vez. Donde PowerShell normalmente usa un solo hilo, hay muchas formas de usar más de uno para paralelizar tu código.

El principal beneficio del multihilo es disminuir el tiempo de ejecución del código. Esta disminución de tiempo es a costa de un mayor requisito de potencia de procesamiento. Cuando se usa el multihilo, muchas acciones se realizan al mismo tiempo, lo que requiere más recursos del sistema.

Por ejemplo, ¿qué pasaría si quisieras crear un nuevo usuario en Active Directory? En este ejemplo, no hay nada para multihilos porque solo se está ejecutando un comando. Todo esto cambia cuando quieres crear 1000 nuevos usuarios.

Sin multihilos, ejecutarías el comando New-ADUser 1000 veces para crear a todos los usuarios. Tal vez tarde tres segundos en crear un nuevo usuario. Para crear los 1000 usuarios, llevaría poco menos de una hora. En lugar de usar un hilo para 1000 comandos, podrías usar 100 hilos, cada uno ejecutando diez comandos. Ahora, en lugar de tomar alrededor de 50 minutos, ¡estarías listo en menos de un minuto!

Nota que no verás una escalabilidad perfecta. El acto de iniciar y cerrar elementos en el código llevará algo de tiempo. Usando un solo hilo, PowerShell necesita ejecutar el código y ya está. Con múltiples hilos, el hilo original utilizado para ejecutar tu consola se utilizará para gestionar los otros hilos. En cierto punto, ese hilo original estará al límite solo manteniendo todos los demás hilos en línea.

Prerrequisitos para el Multihilado en PowerShell

Vas a aprender cómo funciona el multihilado en PowerShell prácticamente en este artículo. Si deseas seguir, a continuación te detallo algunas cosas que necesitarás y algunos detalles sobre el entorno que se está utilizando.

  • Windows Versión 3 o superior de PowerShell: Todo, a menos que se indique explícitamente, funcionará en Windows PowerShell versión 3 o superior. Se utilizará la versión 5.1 de Windows PowerShell para los ejemplos.
  • CPU y memoria adicionales: Necesitarás al menos un poco más de CPU y memoria para paralelizar con PowerShell. Si no tienes esto disponible, es posible que no veas ningún beneficio de rendimiento.

Prioridad #1: ¡Arregla tu código!

Antes de sumergirte en acelerar tus scripts con el multihilo de PowerShell, hay algunas tareas de preparación que querrás completar. Primero, optimiza tu código.

Aunque puedes asignar más recursos a tu código para que se ejecute más rápido, el multihilo agrega mucha complejidad adicional. Si hay formas de acelerar tu código antes del multihilo, deben hacerse primero.

Identificar cuellos de botella

Uno de los primeros pasos para paralelizar tu código es descubrir qué lo está ralentizando. El código podría ser lento debido a lógica incorrecta o bucles adicionales donde puedes realizar algunas modificaciones para permitir una ejecución más rápida antes del multihilo.

Un ejemplo de una forma común de acelerar tu código es desplazar tu filtrado a la izquierda. Si estás interactuando con una gran cantidad de datos, cualquier filtrado que desees permitir para reducir la cantidad de datos debe hacerse lo antes posible. A continuación, se muestra un ejemplo de algún código para obtener la cantidad de CPU utilizada por el proceso svchost.

El siguiente ejemplo está leyendo todos los procesos en ejecución y luego filtrando un solo proceso (svchost). Luego selecciona la propiedad de la CPU y asegura que el valor no sea nulo.

PS51> Get-Process | Where-Object {$_.ProcessName -eq 'svchost'} | 
	Select-Object CPU | Where-Object {$_.CPU -ne $null}

Compara el código anterior con el siguiente ejemplo. A continuación, se muestra otro ejemplo de código que tiene la misma salida pero está dispuesto de manera diferente. Observa que el código a continuación es más sencillo y desplaza toda la lógica posible hacia la izquierda del símbolo de la tubería. Esto evita que Get-Process devuelva procesos que no te interesan.

PS51> Get-Process -Name 'svchost' | Where-Object {$_.CPU -ne $null} | 
	Select-Object CPU

A continuación, se muestra la diferencia de tiempo al ejecutar las dos líneas anteriores. Si bien la diferencia de 117 ms no será perceptible si solo ejecutas este código una vez, comenzará a acumularse si se ejecuta miles de veces.

time difference for running the two lines from above

Usar código seguro para hilos

A continuación, asegúrate de que tu código sea “seguro para hilos”. El término “seguro para hilos” se refiere a si un hilo está ejecutando código, otro hilo puede estar ejecutando el mismo código al mismo tiempo sin causar un conflicto.

Por ejemplo, escribir en el mismo archivo en dos hilos diferentes no es seguro para hilos, ya que no sabrá qué agregar al archivo primero. Mientras que dos hilos leyendo desde un archivo son seguros para hilos, ya que el archivo no se está modificando. Ambos hilos obtienen la misma salida.

El problema con el código de multiproceso en PowerShell que no es seguro para hilos es que puedes obtener resultados inconsistentes. A veces puede funcionar bien debido a que los hilos coinciden en el momento adecuado para no causar un conflicto. Otras veces, habrá un conflicto y dificultará la solución del problema debido a los errores inconsistentes.

Si solo estás ejecutando dos o tres trabajos a la vez, es posible que se alineen perfectamente y todos escriban en el archivo en diferentes momentos. Sin embargo, cuando escalas el código a 20 o 30 trabajos, la probabilidad de que al menos dos de los trabajos intenten escribir al mismo tiempo disminuye considerablemente.

Ejecución paralela con PSJobs

Una de las formas más sencillas de crear subprocesos en un script es utilizando PSJobs. PSJobs cuenta con cmdlets incorporados en el módulo Microsoft.PowerShell.Core. Este módulo está incluido en todas las versiones de PowerShell desde la versión 3. Los comandos de este módulo te permiten ejecutar código en segundo plano mientras se sigue ejecutando código diferente en primer plano. A continuación, puedes ver todos los comandos disponibles.

PS51> Get-Command *-Job
Get-Command *-Job output

Realizar un seguimiento de tus trabajos

Todos los PSJobs se encuentran en uno de los once estados. Estos estados son cómo PowerShell administra los trabajos.

A continuación, encontrarás una lista de los estados más comunes en los que puede encontrarse un trabajo.

  • Completado: el trabajo ha finalizado y se puede recuperar los datos de salida o eliminar el trabajo.
  • Ejecutándose: el trabajo está en ejecución y no se puede eliminar sin detenerlo forzosamente. También, aún no se puede recuperar la salida.
  • Bloqueado: el trabajo aún está en ejecución, pero se solicita información al host antes de poder continuar.
  • Falló: Se produjo un error de terminación durante la ejecución del trabajo.

Para obtener el estado de un trabajo que se ha iniciado, utiliza el comando Get-Job. Este comando obtiene todos los atributos de tus trabajos.

A continuación se muestra la salida de un trabajo donde puedes ver que el estado es Completado. El ejemplo a continuación está ejecutando el código Start-Sleep 5 dentro de un trabajo usando el comando Start-Job. El estado de ese trabajo luego se devuelve utilizando el comando Get-Job.

PS51> Start-Job -Scriptblock {Start-Sleep 5}
PS51> Get-Job
Get-Job output

Cuando el estado del trabajo devuelve Completado, significa que el código en el bloque de script se ejecutó y terminó de ejecutarse. También puedes ver que la propiedad HasMoreData es Falso. Esto significa que no hubo salida para proporcionar después de que terminó el trabajo.

A continuación se muestra un ejemplo de algunos de los otros estados utilizados para describir trabajos. Puedes ver desde la columna Command lo que puede haber causado que algunos de estos trabajos no se completen, como intentar dormir durante abc segundos que resultó en un trabajo fallido.

All jobs

Creación de nuevos trabajos

Como viste anteriormente, el comando Start-Job te permite crear un nuevo trabajo que comienza a ejecutar código en el trabajo. Cuando creas un trabajo, proporcionas un bloque de script que se utiliza para el trabajo. Entonces, PSJob crea un trabajo con un número de identificación único y comienza a ejecutar el trabajo.

El principal beneficio aquí es que lleva menos tiempo ejecutar el comando Start-Job que ejecutar el bloque de script que estamos utilizando. Puedes ver en la siguiente imagen que en lugar de que el comando tarde cinco segundos en completarse, solo tomó .15 segundos para iniciar el trabajo.

How long it takes to create a job

La razón por la que pudo ejecutar el mismo código en una fracción del tiempo fue porque se estaba ejecutando en segundo plano como un PSJob. Tomó .15 segundos configurar y comenzar a ejecutar el código en segundo plano en lugar de ejecutarlo en primer plano y realmente dormir durante cinco segundos.

Recuperación de la salida del trabajo

A veces, el código dentro del trabajo devuelve salida. Puedes recuperar la salida de ese código usando el comando Receive-Job. El comando Receive-Job acepta un PSJob como entrada y luego escribe la salida del trabajo en la consola. Todo lo que fue producido por el trabajo mientras se ejecutaba se ha almacenado para que cuando se recupere el trabajo, produzca toda la información que se almacenó en ese momento.

Un ejemplo de esto sería ejecutar el siguiente código. Esto creará y comenzará un trabajo que escribirá Hola Mundo en la salida. Luego, recupera la salida del trabajo y la muestra en la consola.

$Job = Start-Job -ScriptBlock {Write-Output 'Hello World'}
Receive-Job $Job
Receive-Job output

Creación de trabajos programados

Otra forma de interactuar con PSJobs es a través de una tarea programada. Las tareas programadas son similares a una tarea programada de Windows que se puede configurar con el Programador de tareas. Las tareas programadas crean una manera de programar fácilmente bloques de comandos de PowerShell complejos en una tarea programada. Utilizando una tarea programada, puedes ejecutar un PSJob en segundo plano basado en desencadenadores.

Desencadenadores de trabajo

Los desencadenadores de trabajo pueden ser cosas como una hora específica, cuando un usuario inicia sesión, cuando el sistema arranca y muchos otros. También puedes hacer que los desencadenadores se repitan a intervalos. Todos estos desencadenadores se definen con el comando New-JobTrigger. Este comando se utiliza para especificar un desencadenador que ejecutará la tarea programada. Una tarea programada sin un desencadenador tiene que ejecutarse manualmente, pero cada trabajo puede tener muchos desencadenadores.

Además de tener un desencadenador, aún tendrías un bloque de comandos como el que se usa con un PSJob normal. Una vez que tienes tanto el desencadenador como el bloque de comandos, utilizarías el comando Register-ScheduledJob para crear el trabajo como se muestra en la siguiente sección. Este comando se utiliza para especificar atributos del trabajo programado como el bloque de comandos que se va a ejecutar y los desencadenadores creados con el comando New-JobTrigger.

Demo

Tal vez necesitas algún código de PowerShell que se ejecute cada vez que alguien inicia sesión en una computadora. Puedes crear un trabajo programado para esto.

Para hacer esto, primero definirías un desencadenador usando New-JobTrigger y definirías el trabajo programado como se muestra a continuación. Este trabajo programado escribirá una línea en un archivo de registro cada vez que alguien inicie sesión.

$Trigger = New-JobTrigger -AtLogon
$Script = {"User $env:USERNAME logged in at $(Get-Date -Format 'y-M-d H:mm:ss')" | Out-File -FilePath C:\Temp\Login.log -Append}

Register-ScheduledJob -Name Log_Login -ScriptBlock $Script -Trigger $Trigger

Una vez que ejecutes los comandos anteriores, obtendrás una salida similar a cuando se crea un nuevo trabajo, que mostrará el ID del trabajo, el bloque de script y algunos otros atributos como se muestra a continuación.

Registering a scheduled job

Después de algunos intentos de inicio de sesión, puedes ver en la captura de pantalla a continuación que ha registrado los intentos.

Example login.log text file

Aprovechando el parámetro AsJob

Otra forma de usar trabajos es utilizar el parámetro AsJob que está integrado en muchos comandos de PowerShell. Dado que hay muchos comandos diferentes, puedes encontrar todos ellos usando Get-Command como se muestra a continuación.

PS51> Get-Command -ParameterName AsJob

Uno de los comandos más prevalentes es Invoke-Command. Normalmente, cuando ejecutas este comando, comenzará a ejecutar un comando de inmediato. Mientras que algunos comandos devolverán de inmediato, permitiéndote continuar con lo que estabas haciendo, otros esperarán hasta que el comando haya terminado.

Usar el parámetro AsJob hace exactamente lo que parece y ejecuta el comando ejecutado como un trabajo en lugar de ejecutarlo de forma síncrona en la consola.

Aunque la mayor parte del tiempo AsJob se puede usar con la máquina local, Invoke-Command no tiene una opción nativa para ejecutarse en la máquina local. Hay un método alternativo usando Localhost como el valor del parámetro ComputerName. A continuación se muestra un ejemplo de este método alternativo.

PS51> Invoke-Command -ScriptBlock {Start-Sleep 5} -ComputerName localhost

Para mostrar el parámetro AsJob en acción, el siguiente ejemplo utiliza Invoke-Command para dormir durante cinco segundos y luego repetir el mismo comando usando AsJob para mostrar la diferencia en los tiempos de ejecución.

PS51> Measure-Command {Invoke-Command -ScriptBlock {Start-Sleep 5}}
PS51> Measure-Command {Invoke-Command -ScriptBlock {Start-Sleep 5} -AsJob -ComputerName localhost}
Comparing speed of jobs

Runspaces: ¡Algo así como trabajos pero más rápido!

Hasta ahora, has estado aprendiendo formas de utilizar hilos adicionales con PowerShell solo usando los comandos integrados. Otra opción para ejecutar en múltiples hilos tu script es utilizar un runspace separado.

Runspaces son el área cerrada en la que operan los hilos que ejecutan PowerShell. Mientras que el runspace que se utiliza con la consola de PowerShell está restringido a un solo hilo, puedes usar runspaces adicionales para permitir el uso de hilos adicionales.

Runspace vs PSJobs

Aunque un runspace y un PSJob comparten muchas similitudes, hay algunas diferencias importantes en el rendimiento. La mayor diferencia entre runspaces y PSJobs es el tiempo que lleva configurar y descomponer cada uno.

En el ejemplo de la sección anterior, el PSJob creado tardó alrededor de 150 ms en iniciarse. Este es un mejor caso ya que el scriptblock para el trabajo no incluía mucho código y no se pasaron variables adicionales al trabajo.

En contraste con la creación del PSJob, un runspace se crea de antemano. La mayoría del tiempo que lleva iniciar un trabajo de runspace se maneja antes de agregar cualquier código.

A continuación se muestra un ejemplo de cómo ejecutar el mismo comando que usamos para el PSJob en lugar de en el runspace.

Measuring speed of job creation

En cambio, a continuación se muestra el código utilizado para la versión de runspace. Puedes observar que hay mucho más código para ejecutar la misma tarea. Pero el beneficio del código adicional reduce casi en 3/4 el tiempo, permitiendo que el comando comience a ejecutarse en 36 ms en comparación con los 148 ms.

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})
$PowerShell.BeginInvoke()
Measuring speed of creating a runspace

Ejecución de Runspaces: Un Recorrido

Utilizar runspaces puede ser una tarea desalentadora al principio, ya que no hay más ayuda de comandos de PowerShell. Tendrás que lidiar directamente con las clases .NET. En esta sección, desglosemos lo que se necesita para crear un runspace en PowerShell.

En este recorrido, crearás un runspace separado de tu consola de PowerShell y una instancia separada de PowerShell. Luego asignarás el nuevo runspace a la nueva instancia de PowerShell y agregarás código a esa instancia.

Crear el Runspace

Lo primero que debes hacer es crear tu nuevo runspace. Esto se hace utilizando la clase runspacefactory. Guárdalo en una variable, como se muestra a continuación, para que pueda ser referenciado más tarde.

 $Runspace = [runspacefactory]::CreateRunspace()

Ahora que el runspace está creado, asígnalo a una instancia de PowerShell para ejecutar código de PowerShell. Para esto, usarás la clase powershell y, al igual que con el runspace, deberás almacenarlo en una variable, como se muestra a continuación.

 $PowerShell = [powershell]::Create()

Luego, agrega el runspace a tu instancia de PowerShell, abre el runspace para poder ejecutar código y agrega tu scriptblock. Esto se muestra a continuación con un scriptblock para dormir durante cinco segundos.

 $PowerShell.Runspace = $Runspace
 $Runspace.Open()
 $PowerShell.AddScript({Start-Sleep 5})

Ejecutar el Runspace

Hasta ahora, el bloque de script aún no se ha ejecutado. Todo lo que se ha hecho hasta ahora es definir todo para el espacio de ejecución. Para comenzar a ejecutar el bloque de script, tienes dos opciones.

  • Invoke() – El método Invoke() ejecuta el bloque de script en el espacio de ejecución, pero espera para regresar a la consola hasta que el espacio de ejecución regrese. Esto es bueno para realizar pruebas y asegurarse de que tu código se esté ejecutando correctamente antes de dejarlo en libertad.
  • BeginInvoke() – Utilizar el método BeginInvoke() es lo que querrás hacer para ver realmente un aumento de rendimiento. Esto iniciará la ejecución del bloque de script en el espacio de ejecución y te devolverá inmediatamente a la consola.

Cuando uses BeginInvoke(), almacena la salida en una variable, ya que se necesitará para ver el estado del bloque de script en el espacio de ejecución, como se muestra a continuación.

$Job = $PowerShell.BeginInvoke()

Una vez que tengas la salida de BeginInvoke() almacenada en una variable, puedes verificar esa variable para ver el estado del trabajo, como se muestra a continuación en la propiedad IsCompleted.

the job is completed

Otra razón por la que necesitarás almacenar la salida en una variable es porque, a diferencia del método Invoke(), BeginInvoke() no devolverá automáticamente la salida cuando el código haya terminado. Para hacer esto, debes usar el método EndInvoke() una vez que se haya completado.

En este ejemplo, no habría salida, pero para finalizar la invocación, usarías el comando a continuación.

$PowerShell.EndInvoke($Job)

Una vez que todas las tareas que encolaste en el espacio de ejecución hayan terminado, siempre debes cerrar el espacio de ejecución. Esto permitirá que el proceso automatizado de recolección de basura de PowerShell limpie los recursos no utilizados. A continuación se muestra el comando que usarías para hacer esto.

$Runspace.Close()

Usando Conjuntos de Espacios de Ejecución

Aunque el uso de un espacio de ejecución mejora el rendimiento, se enfrenta a una limitación importante de un solo hilo. Aquí es donde los conjuntos de espacios de ejecución brillan en su uso de múltiples hilos.

En la sección anterior, solo estabas utilizando dos espacios de ejecución. Solo usaste uno para la consola de PowerShell en sí misma y otro que habías creado manualmente. Los conjuntos de espacios de ejecución te permiten tener varios espacios de ejecución gestionados en segundo plano usando una única variable.

Aunque este comportamiento de múltiples espacios de ejecución se puede lograr con varios objetos de espacio de ejecución, usar un conjunto de espacios de ejecución hace que la gestión sea mucho más fácil.

Los conjuntos de espacios de ejecución difieren de los espacios de ejecución individuales en cómo se configuran. Una de las diferencias clave es que defines la cantidad máxima de hilos que se pueden utilizar para el conjunto de espacios de ejecución. Con un solo espacio de ejecución, está limitado a un solo hilo, pero con un conjunto puedes especificar la cantidad máxima de hilos a los que puede escalar el conjunto.

La cantidad recomendada de hilos en un conjunto de espacios de ejecución depende de la cantidad de tareas que se estén realizando y de la máquina en la que se esté ejecutando el código. Aunque aumentar la cantidad máxima de hilos no afectará negativamente la velocidad en la mayoría de los casos, es posible que tampoco veas ningún beneficio.

Demostración de Velocidad del Conjunto de Espacios de Ejecución

Para mostrar un ejemplo de dónde un grupo de espacios de ejecución superará a un solo espacio de ejecución, tal vez desees crear diez archivos nuevos. Si usaras un solo espacio de ejecución para esta tarea, crearías el primer archivo, luego pasarías al segundo, y luego al tercero, y así sucesivamente hasta que se crearan los diez. El bloque de script para este ejemplo podría parecer algo así. Alimentarías este bloque de script con diez nombres de archivo en un bucle y todos se crearían.

$Scriptblock = {
    param($Name)
    New-Item -Name $Name -ItemType File
}

En el siguiente ejemplo, se define un bloque de script que contiene un script corto que acepta un nombre y crea un archivo con ese nombre. Se crea un grupo de espacios de ejecución con un máximo de 5 hilos.

A continuación, un bucle se ejecuta diez veces y cada vez asignará el número de la iteración a $_. Así que tendría 1 en la primera iteración, 2 en la segunda, y así sucesivamente.

El bucle crea un objeto PowerShell, asigna el bloque de script y el argumento para el script, y comienza el proceso.

Finalmente, al final del bucle, esperará a que todas las tareas de la cola terminen.

$Scriptblock = {
    param($Name)
    New-Item -Name $Name -ItemType File
}

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$RunspacePool.Open()
$Jobs = @()

1..10 | Foreach-Object {
	$PowerShell = [powershell]::Create()
	$PowerShell.RunspacePool = $RunspacePool
	$PowerShell.AddScript($ScriptBlock).AddArgument($_)
	$Jobs += $PowerShell.BeginInvoke()
}

while ($Jobs.IsCompleted -contains $false) {
	Start-Sleep 1
}

Ahora, en lugar de crear hilos uno a la vez, creará cinco a la vez. Sin grupos de espacios de ejecución, tendrías que crear y gestionar cinco espacios de ejecución separados y cinco instancias separadas de Powershell. Esta gestión rápidamente se convierte en un lío.

En cambio, puedes crear un grupo de espacios de ejecución, una instancia de PowerShell, usar el mismo bloque de código y el mismo bucle. La diferencia es que el espacio de ejecución se expandirá para utilizar todos los cinco de esos hilos por sí mismo.

Creación de Grupos de Espacios de Ejecución

La creación de un pool de runspaces es muy similar al runspace que se creó en una sección anterior. A continuación se muestra un ejemplo de cómo hacerlo. La adición de un scriptblock y el proceso de invocación es idéntico a un runspace. Como puedes ver a continuación, el pool de runspaces se está creando con un máximo de cinco hilos.

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$RunspacePool.Open()

Comparación de Runspaces y Pools de Runspaces para Velocidad

Para mostrar la diferencia entre un runspace y un pool de runspaces, crea un runspace y ejecuta el comando Start-Sleep de antes. Esta vez, sin embargo, debe ejecutarse 10 veces. Como puedes ver en el código siguiente, se está creando un runspace que dormirá durante 5 segundos.

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})

1..10 | Foreach-Object {
    $Job = $PowerShell.BeginInvoke()
    while ($Job.IsCompleted -eq $false) {Start-Sleep -Milliseconds 100}
}

Nótese que al utilizar un solo runspace, tendrás que esperar hasta que se complete antes de que se pueda iniciar otra invocación. Por eso se añade una espera de 100 ms hasta que se complete el trabajo. Aunque esto se puede reducir, verás retornos decrecientes ya que pasarás más tiempo comprobando si el trabajo está hecho que esperando a que el trabajo termine.

Del ejemplo siguiente, se puede ver que tardó unos 51 segundos en completar 10 conjuntos de 5 segundos de espera.

Measuring performance of creating runspaces

Ahora, en lugar de usar un solo runspace, cambia a un pool de runspaces. A continuación se muestra el código que se va a ejecutar. Puedes ver que hay algunas diferencias entre el uso de los dos en el código siguiente al utilizar un pool de runspaces.

$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
$Jobs = @()

1..10 | Foreach-Object {
    $PowerShell = [powershell]::Create()
    $PowerShell.RunspacePool = $RunspacePool
    $PowerShell.AddScript({Start-Sleep 5})
    $Jobs += $PowerShell.BeginInvoke()
}
while ($Jobs.IsCompleted -contains $false) {Start-Sleep -Milliseconds 100}

Como puedes ver a continuación, esto se completa en poco más de 10 segundos, lo cual es una mejora considerable respecto a los 51 segundos del runspace único.

Comparing runspaces vs. runspace pools

A continuación se presenta un resumen detallado de la diferencia entre un runspace y un pool de runspaces en estos ejemplos.

Property Runspace Runspace Pool
Wait Delay Waiting for each job to finish before continuing to the next. Starting all of the jobs and then waiting until they have all finished.
Amount of Threads One Five
Runtime 50.8 Seconds 10.1 Seconds

Adaptarse a los Runspaces con PoshRSJob

A frequent occurrence when programming is that you will do what is more comfortable and accept the small loss in performance. This could be because it makes the code easier to write or easier to read, or it could just be your preference.

Lo mismo sucede con PowerShell donde algunas personas usarán PSJobs en lugar de runspaces debido a la facilidad de uso. Hay algunas cosas que se pueden hacer para encontrar un punto medio y obtener un mejor rendimiento sin que sea demasiado difícil de usar.

Existe un módulo ampliamente utilizado llamado PoshRSJob que contiene módulos que coinciden con el estilo de PSJobs normales pero con el beneficio adicional de utilizar runspaces. En lugar de tener que especificar todo el código para crear el runspace y el objeto powershell, el módulo PoshRSJob se encarga de hacer todo eso cuando ejecutas los comandos.

Para instalar el módulo, ejecuta el siguiente comando en una sesión de PowerShell administrativa.

Install-Module PoshRSJob

Una vez instalado el módulo, puedes ver que los comandos son iguales que los comandos PSJob con un prefijo RS. En lugar de Start-Job es Start-RSJob. En lugar de Get-Job es Get-RSJob.

A continuación, se muestra un ejemplo de cómo ejecutar el mismo comando en un PSJob y luego nuevamente en un RSJob. Como puedes ver, tienen una sintaxis y una salida muy similares, pero no son completamente idénticas.

run the same command in a PSJob and then again in an RSJob

A continuación, se muestra un código que se puede utilizar para comparar la diferencia de velocidad entre un PSJob y un RSJob.

Measure-Command {Start-Job -ScriptBlock {Start-Sleep 5}}
Measure-Command {Start-RSJob -ScriptBlock {Start-Sleep 5}}

Como puedes ver a continuación, hay una gran diferencia de velocidad ya que los RSJobs todavía están utilizando runspaces debajo de la superficie.

large speed difference since the RSJobs are still using runspaces below the covers

Foreach-Object -Paralelo

La comunidad de PowerShell ha estado deseando una manera más fácil e integrada de multihilar un proceso. El interruptor paralelo es lo que ha surgido de eso.

Al momento de escribir esto, PowerShell 7 todavía está en vista previa, pero han añadido un parámetro Paralelo al comando Foreach-Object. Este proceso utiliza espacios de ejecución para paralelizar el código y utiliza el bloque de comandos utilizado para el Foreach-Object como el bloque de comandos para el espacio de ejecución.

Aunque los detalles aún se están trabajando, esta puede ser una forma más fácil de usar espacios de ejecución en el futuro. Como puedes ver a continuación, puedes recorrer rápidamente muchos conjuntos de pausas.

Measure-Command {1..10 | Foreach-Object {Start-Sleep 5}}
Measure-Command {1..10 | Foreach-Object -Parallel {Start-Sleep 5}}
Looping through and measuring commands

Desafíos con la Multihilatura

Aunque hasta ahora la multihilatura ha sonado como algo increíble, este no es del todo el caso. Hay muchos desafíos que vienen junto con la multihilatura de cualquier código.

Uso de Variables

Uno de los desafíos más grandes y evidentes con la multihilatura es que no puedes compartir variables sin pasarlas como argumentos. Hay una excepción con una tabla hash sincronizada, pero eso es tema para otro día.

Tanto los PSJobs como los espacios de ejecución operan sin acceso a variables existentes y no hay forma de interactuar con variables utilizadas en espacios de ejecución diferentes desde tu consola.

Esto plantea un gran desafío para pasar información dinámicamente a estos trabajos. La respuesta es diferente dependiendo de qué tipo de multihilatura estés utilizando.

Para Start-Job y Start-RSJob del módulo PoshRSJob, puedes usar el parámetro ArgumentList para proporcionar una lista de objetos que se pasarán como parámetros al scriptblock en el orden que los listes. A continuación se muestran ejemplos de los comandos utilizados para PSJobs y RSJobs.

PSJob:

Start-Job -Scriptblock {param ($Text) Write-Output $Text} -ArgumentList "Hello world!"

RSJob:

Start-RSJob -Scriptblock {param ($Text) Write-Output $Text} -ArgumentList "Hello world!"

Los runspaces nativos no te brindan la misma facilidad. En su lugar, debes usar el método AddArgument() en el objeto PowerShell. A continuación, se muestra un ejemplo de cómo se vería para cada uno.

Runspace:

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({param ($Text) Write-Output $Text})
$PowerShell.AddArgument("Hello world!")
$PowerShell.BeginInvoke()

Aunque los grupos de runspaces funcionan de la misma manera, a continuación se muestra un ejemplo de cómo agregar un argumento a un grupo de runspaces.

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$RunspacePool.Open()
$PowerShell.AddScript({param ($Text) Write-Output $Text})
$PowerShell.AddArgument("Hello world!")
$PowerShell.BeginInvoke()

Registro

La multihilos también introduce desafíos de registro. Dado que cada hilo está operando independientemente de los demás, no todos pueden registrar en el mismo lugar. Si intentaras registrar, por ejemplo, en un archivo con múltiples hilos, cada vez que un hilo estuviera escribiendo en el archivo, ningún otro hilo podría hacerlo. Esto podría ralentizar tu código o hacer que falle por completo.

Como ejemplo, a continuación tienes un código para intentar registrar 100 veces en un solo archivo usando 5 hilos en un grupo de runspaces.

$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
1..100 | Foreach-Object {
	$PowerShell = [powershell]::Create().AddScript({'Hello' | Out-File -Append -FilePath .\Test.txt})
	$PowerShell.RunspacePool = $RunspacePool
	$PowerShell.BeginInvoke()
}
$RunspacePool.Close()

Desde la salida no verás errores, pero si observas el tamaño del archivo de texto, verás que no todos los 100 trabajos se completaron correctamente.

Get-Content -Path .\Test.txt).Count” class=”wp-image-3241″/>
(Get-Content -Path .\Test.txt).Count

Algunas formas de solucionar esto son registrar en archivos separados. Esto elimina el problema de bloqueo de archivos, pero entonces tendrías muchos archivos de registro por revisar para averiguar todo lo que sucedió.

Otra alternativa es permitir que el tiempo de salida se desincronice y solo registrar lo que hizo un trabajo una vez que haya terminado. Esto te permite tener todo serializado a través de tu sesión original, pero pierdes algunos detalles porque no necesariamente sabes en qué orden ocurrió todo.

Resumen

Aunque la multihilo puede proporcionar grandes mejoras de rendimiento, también puede causar dolores de cabeza. Mientras que algunas cargas de trabajo se beneficiarán enormemente, otras no lo harán en absoluto. Hay muchos pros y contras en el uso de la multihilo, pero si se utiliza correctamente, se puede reducir drásticamente el tiempo de ejecución de tu código.

Lecturas adicionales

Source:
https://adamtheautomator.com/powershell-multithreading/