Multithreading em PowerShell: Uma Profundidade Detalhada

Em algum momento, a maioria das pessoas encontrará um problema em que um script básico do PowerShell é muito lento para resolver. Isso pode ser coletar dados de muitos computadores em sua rede ou talvez criar muitos novos usuários no Active Directory de uma vez. Esses são ótimos exemplos de onde usar mais poder de processamento faria seu código rodar mais rápido. Vamos ver como resolver isso usando multithreading no PowerShell!

A sessão padrão do PowerShell é single-threaded. Ele executa um comando e, quando termina, passa para o próximo comando. Isso é bom, pois mantém tudo repetível e não usa muitos recursos. Mas e se as ações que ele está executando não forem dependentes umas das outras e você tiver recursos de CPU disponíveis? Nesse caso, é hora de começar a pensar em multithreading.

Neste artigo, você aprenderá como entender e usar várias técnicas de multithreading no PowerShell para processar várias sequências de dados ao mesmo tempo, mas gerenciadas pela mesma console.

Entendendo o Multithreading no PowerShell

O multithreading é uma maneira de executar mais de um comando ao mesmo tempo. Onde o PowerShell normalmente usa um único thread, existem várias maneiras de usar mais de um para paralelizar o código.

O principal benefício do multithreading é diminuir o tempo de execução do código. Essa diminuição do tempo é compensada por um requisito maior de poder de processamento. Ao usar multithreading, muitas ações são executadas ao mesmo tempo, o que exige mais recursos do sistema.

Por exemplo, e se você quisesse criar um novo usuário no Active Directory? Neste exemplo, não há nada para realizar multi-threading porque apenas um comando está sendo executado. Isso muda quando você deseja criar 1000 novos usuários.

Sem multi-threading, você executaria o comando New-ADUser 1000 vezes para criar todos os usuários. Talvez leve três segundos para criar um novo usuário. Para criar todos os 1000 usuários, levaria pouco menos de uma hora. Em vez de usar uma thread para 1000 comandos, você pode usar 100 threads, cada uma executando dez comandos. Agora, em vez de levar cerca de 50 minutos, você levará menos de um minuto!

Observe que você não verá uma escalabilidade perfeita. A ação de iniciar e encerrar itens no código levará algum tempo. Usando uma única thread, o PowerShell precisa executar o código e está pronto. Com várias threads, a thread original usada para executar seu console será usada para gerenciar as outras threads. Em um certo ponto, essa thread original estará no máximo apenas mantendo todas as outras threads em ordem.

Pré-requisitos para o Multi-threading no PowerShell

Neste artigo, você aprenderá como funciona o multi-threading no PowerShell, de forma prática. Se você quiser acompanhar, abaixo estão algumas coisas que você vai precisar e alguns detalhes sobre o ambiente que está sendo usado.

  • Windows PowerShell versão 3 ou superior – A menos que explicitamente indicado, todo o código demonstrado funcionará no Windows PowerShell versão 3 ou superior. A versão 5.1 do Windows PowerShell será usada para os exemplos.
  • Recursos de CPU e memória extras – Você precisará de pelo menos um pouco de CPU e memória extras para paralelizar com o PowerShell. Se você não tiver isso disponível, talvez não veja nenhum benefício de desempenho.

Prioridade nº 1: Corrigir seu código!

Antes de se aprofundar na aceleração de seus scripts com multithreading no PowerShell, há algumas etapas preparatórias que você desejará concluir. Primeiro, otimize seu código.

Embora você possa alocar mais recursos para o seu código para fazê-lo rodar mais rápido, o multithreading traz uma complexidade extra. Se houver maneiras de acelerar seu código antes do multithreading, elas devem ser feitas primeiro.

Identificar gargalos

Um dos primeiros passos para paralelizar seu código é descobrir o que está tornando-o lento. O código pode estar lento devido a lógica ruim ou loops extras nos quais você pode fazer algumas modificações para permitir uma execução mais rápida antes do multithreading.

Um exemplo de uma maneira comum de acelerar seu código é deslocar seu filtro para a esquerda. Se você estiver interagindo com um monte de dados, qualquer filtragem que você queira permitir para reduzir a quantidade de dados deve ser feita o mais cedo possível. Abaixo está um exemplo de algum código para obter a quantidade de CPU usada pelo processo svchost.

O exemplo abaixo está lendo todos os processos em execução e, em seguida, filtrando um único processo (svchost). Em seguida, ele seleciona a propriedade CPU e garante que o valor não seja nulo.

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

Compare o código acima com o exemplo abaixo. Abaixo está outro exemplo de código que tem a mesma saída, mas está organizado de forma diferente. Observe que o código abaixo é mais simples e desloca toda a lógica possível para a esquerda do símbolo de pipe. Isso impede que o Get-Process retorne processos que você não se importa.

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

Abaixo está a diferença de tempo para executar as duas linhas acima. Embora a diferença de 117ms não seja perceptível se você executar esse código apenas uma vez, ela começará a se acumular se for executado milhares de vezes.

time difference for running the two lines from above

Usando Código Thread-Safe

Em seguida, certifique-se de que seu código seja “thread-safe”. O termo “thread-safe” refere-se a se uma thread está executando o código, outra thread pode estar executando o mesmo código ao mesmo tempo e não causar um conflito.

Por exemplo, gravar no mesmo arquivo em duas threads diferentes não é thread-safe, pois ele não saberá o que adicionar ao arquivo primeiro. Enquanto duas threads lendo de um arquivo são thread-safe, pois o arquivo não está sendo alterado. Ambas as threads obtêm a mesma saída.

O problema com o código de multithreading do PowerShell que não é thread-safe é que você pode obter resultados inconsistentes. Às vezes, pode funcionar bem devido às threads apenas acontecerem de acertar o momento certo para não causar um conflito. Outras vezes, você terá um conflito e isso tornará a solução de problemas difícil devido aos erros inconsistentes.

Se você estiver executando apenas dois ou três trabalhos ao mesmo tempo, pode ser que eles acabem se alinhando perfeitamente, onde todos eles estão gravando no arquivo em momentos diferentes. Então, quando você escala o código para 20 ou 30 trabalhos, a probabilidade de pelo menos dois trabalhos tentarem gravar ao mesmo tempo diminui significativamente.

Execução Paralela com PSJobs

Uma das maneiras mais fáceis de criar um script multithread é com PSJobs. Os PSJobs possuem cmdlets incorporados no módulo Microsoft.PowerShell.Core. O módulo Microsoft.PowerShell.Core está incluído em todas as versões do PowerShell desde a versão 3. Os comandos deste módulo permitem que você execute código em segundo plano enquanto continua a executar código diferente em primeiro plano. Você pode ver todos os comandos disponíveis abaixo.

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

Acompanhando seus trabalhos

Todos os PSJobs estão em um dos onze estados. Esses estados são como o PowerShell gerencia os trabalhos.

Abaixo, você encontrará uma lista dos estados mais comuns em que um trabalho pode estar.

  • Concluído – O trabalho foi concluído e os dados de saída podem ser obtidos ou o trabalho pode ser removido.
  • Executando – O trabalho está sendo executado no momento e não pode ser removido sem parar forçadamente o trabalho. A saída também não pode ser obtida ainda.
  • Bloqueado – O trabalho ainda está em execução, mas o host está aguardando informações antes de poder prosseguir.
  • Falhou – Ocorreu um erro de terminação durante a execução do trabalho.

Para obter o status de um trabalho que foi iniciado, você usa o comando Get-Job. Este comando obtém todos os atributos dos seus trabalhos.

Abaixo está a saída de um trabalho onde você pode ver que o estado é Concluído. O exemplo abaixo está executando o código Start-Sleep 5 dentro de um trabalho usando o comando Start-Job. O status desse trabalho está sendo retornado usando o comando Get-Job.

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

Quando o status do trabalho retorna Concluído, isso significa que o código no bloco de script foi executado e concluído. Você também pode ver que a propriedade HasMoreData é Falso. Isso significa que não houve saída a ser fornecida após a conclusão do trabalho.

Abaixo está um exemplo de alguns dos outros estados usados para descrever trabalhos. Você pode ver na coluna Command que o que pode ter causado a falha em alguns desses trabalhos, como tentar esperar por abc segundos resultou em um trabalho falhou.

All jobs

Criando Novos Trabalhos

Como você viu acima, o comando Start-Job permite que você crie um novo trabalho que comece a executar código no trabalho. Ao criar um trabalho, você fornece um bloco de script a ser usado para o trabalho. O PSJob então cria um trabalho com um número de ID único e começa a executar o trabalho.

O principal benefício aqui é que leva menos tempo para executar o comando Start-Job do que para executar o scriptblock que estamos usando. Você pode ver na imagem abaixo que, em vez do comando levar cinco segundos para ser concluído, levou apenas 0,15 segundos para iniciar o trabalho.

How long it takes to create a job

A razão pela qual ele conseguiu executar o mesmo código em uma fração do tempo foi porque ele estava sendo executado em segundo plano como um PSJob. Levou 0,15 segundos para configurar e iniciar a execução do código em segundo plano, em vez de executá-lo em primeiro plano e realmente esperar por cinco segundos.

Recuperando a saída do trabalho

Às vezes, o código dentro do trabalho retorna saída. Você pode recuperar a saída desse código usando o comando Receive-Job. O comando Receive-Job aceita um PSJob como entrada e, em seguida, grava a saída do trabalho no console. Qualquer coisa que tenha sido produzida pelo trabalho enquanto ele estava sendo executado foi armazenada para que, quando o trabalho for recuperado, ele exiba tudo o que foi armazenado naquele momento.

Um exemplo disso seria executar o código abaixo. Isso criará e iniciará um trabalho que escreverá Hello World na saída. Em seguida, ele recupera a saída do trabalho e a exibe no console.

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

Criando trabalhos agendados

Outra forma de interagir com PSJobs é através de um trabalho agendado. Trabalhos agendados são semelhantes a uma tarefa agendada do Windows que pode ser configurada com o Agendador de Tarefas. Trabalhos agendados criam uma forma de agendar facilmente blocos de script PowerShell complexos em uma tarefa agendada. Usando um trabalho agendado, você pode executar um PSJob em segundo plano com base em gatilhos.

Gatilhos de Trabalho

Os gatilhos de trabalho podem ser coisas como um horário específico, quando um usuário faz login, quando o sistema é iniciado e muitos outros. Você também pode fazer com que os gatilhos se repitam em intervalos. Todos esses gatilhos são definidos com o comando New-JobTrigger. Este comando é usado para especificar um gatilho que irá executar o trabalho agendado. Um trabalho agendado sem um gatilho precisa ser executado manualmente, mas cada trabalho pode ter vários gatilhos.

Além de ter um gatilho, você ainda teria um bloco de script, assim como é usado em um PSJob normal. Quando você tiver tanto o gatilho quanto o bloco de script, você usaria o comando Register-ScheduledJob para criar o trabalho, como mostrado na próxima seção. Este comando é usado para especificar atributos do trabalho agendado, como o bloco de script que será executado e os gatilhos criados com o comando New-JobTrigger.

Demonstração

Talvez você precise de algum código PowerShell para ser executado toda vez que alguém fizer login em um computador. Você pode criar um trabalho agendado para isso.

Para fazer isso, você primeiro definiria um acionador usando New-JobTrigger e definiria o trabalho agendado como mostrado abaixo. Este trabalho agendado escreverá uma linha em um arquivo de log sempre que alguém fizer login.

$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

Depois de executar os comandos acima, você obterá uma saída semelhante à criação de um novo trabalho que mostrará o ID do trabalho, o bloco de script e alguns outros atributos, conforme mostrado abaixo.

Registering a scheduled job

Após algumas tentativas de login, você pode ver na captura de tela abaixo que ele registrou as tentativas.

Example login.log text file

Aproveitando o parâmetro AsJob

Outra maneira de usar trabalhos é usar o parâmetro AsJob que é embutido em muitos comandos do PowerShell. Como existem muitos comandos diferentes, você pode encontrá-los todos usando Get-Command, conforme mostrado abaixo.

PS51> Get-Command -ParameterName AsJob

Um dos comandos mais prevalentes é Invoke-Command. Normalmente, quando você executa este comando, ele começará a executar um comando imediatamente. Enquanto alguns comandos retornarão imediatamente, permitindo que você continue com o que estava fazendo, alguns esperarão até que o comando seja concluído.

O uso do parâmetro AsJob faz exatamente o que parece e executa o comando executado como um trabalho em vez de executá-lo sincronamente no console.

Embora na maioria das vezes AsJob possa ser usado com a máquina local, Invoke-Command não possui uma opção nativa para ser executado na máquina local. Existe uma solução alternativa usando Localhost como valor do parâmetro ComputerName. Abaixo está um exemplo dessa solução alternativa.

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

Para mostrar o parâmetro AsJob em ação, o exemplo abaixo usa Invoke-Command para esperar cinco segundos e, em seguida, repetir o mesmo comando usando AsJob para mostrar a diferença nos tempos de execução.

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: Mais ou Menos como Jobs, mas Mais Rápido!

Até agora, você aprendeu maneiras de usar threads adicionais com o PowerShell apenas usando os comandos integrados. Outra opção para tornar seu script multithread é usar um runspace separado.

Runspaces são a área fechada em que as thread(s) que executam o PowerShell operam. Embora o runspace usado com o console do PowerShell seja restrito a uma única thread, você pode usar runspaces adicionais para permitir o uso de threads adicionais.

Runspace vs PSJobs

Embora um runspace e um PSJob compartilhem muitas semelhanças, há algumas grandes diferenças de desempenho. A maior diferença entre runspaces e PSjobs é o tempo que leva para configurar e encerrar cada um.

No exemplo da seção anterior, o PSjob criado levou cerca de 150ms para ser iniciado. Isso é o melhor caso, já que o scriptblock para o job não incluía muito código e não havia variáveis adicionais sendo passadas para o job.

Em contraste com a criação do PSJob, um runspace é criado antecipadamente. A maior parte do tempo necessário para iniciar um job de runspace é tratada antes que qualquer código seja adicionado.

Abaixo está um exemplo de execução do mesmo comando que usamos para o PSJob no runspace.

Measuring speed of job creation

Por contraste, abaixo está o código usado para a versão do runspace. Você pode ver que há muito mais código para executar a mesma tarefa. Mas o benefício do código extra economiza quase 3/4 do tempo, permitindo que o comando comece a ser executado em 36ms em vez de 148ms.

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

Executando Runspaces: Um Passo a Passo

Usar runspaces pode ser uma tarefa assustadora no início, pois não há mais suporte direto do comando do PowerShell. Você terá que lidar diretamente com as classes .NET. Nesta seção, vamos analisar o que é necessário para criar um runspace no PowerShell.

Neste passo a passo, você vai criar um runspace separado do seu console do PowerShell e uma instância separada do PowerShell. Em seguida, você irá atribuir o novo runspace à nova instância do PowerShell e adicionar código a essa instância.

Crie o Runspace

A primeira coisa que você precisa fazer é criar o seu novo runspace. Você faz isso usando a classe runspacefactory. Armazene isso em uma variável, como mostrado abaixo, para que possa ser referenciado posteriormente.

 $Runspace = [runspacefactory]::CreateRunspace()

Agora que o runspace está criado, atribua-o a uma instância do PowerShell para executar código do PowerShell. Para isso, você usará a classe powershell e, assim como o runspace, precisará armazenar isso em uma variável, como mostrado abaixo.

 $PowerShell = [powershell]::Create()

Em seguida, adicione o runspace à sua instância do PowerShell, abra o runspace para poder executar código e adicione seu scriptblock. Isso é mostrado abaixo com um scriptblock para dormir por cinco segundos.

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

Execute o Runspace

Até agora, o bloco de script ainda não foi executado. Tudo o que foi feito até agora é definir tudo para o espaço de execução. Para começar a executar o bloco de script, você tem duas opções.

  • Invoke() – O método Invoke() executa o bloco de script no espaço de execução, mas aguarda para retornar ao console até que o espaço de execução retorne. Isso é bom para testar para garantir que seu código esteja sendo executado corretamente antes de liberá-lo.
  • BeginInvoke() – Usar o método BeginInvoke() é o que você deseja para realmente ver um ganho de desempenho. Isso iniciará a execução do bloco de script no espaço de execução e imediatamente o retornará ao console.

Ao usar BeginInvoke(), armazene a saída em uma variável, pois será necessário para ver o status do bloco de script no espaço de execução, como mostrado abaixo.

$Job = $PowerShell.BeginInvoke()

Depois de armazenar a saída do BeginInvoke() em uma variável, você pode verificar essa variável para ver o status do trabalho, conforme mostrado abaixo na propriedade IsCompleted.

the job is completed

Outro motivo pelo qual você precisará armazenar a saída em uma variável é porque, ao contrário do método Invoke(), BeginInvoke() não retornará automaticamente a saída quando o código for concluído. Para fazer isso, você deve usar o método EndInvoke() quando ele for concluído.

Neste exemplo, não haveria saída, mas para encerrar a invocação você usaria o comando abaixo.

$PowerShell.EndInvoke($Job)

Uma vez que todas as tarefas que você enfileirou no runspace forem concluídas, você sempre deve fechar o runspace. Isso permitirá que o processo de coleta automática de lixo do PowerShell limpe os recursos não utilizados. Abaixo está o comando que você usaria para fazer isso.

$Runspace.Close()

Usando Pools de Runspace

Embora o uso de um runspace melhore o desempenho, ele encontra uma limitação importante de um único thread. É aí que os pools de runspace se destacam em seu uso de vários threads.

Na seção anterior, você estava usando apenas dois runspaces. Você usou apenas um para o console do PowerShell em si e o que você havia criado manualmente. Os pools de runspace permitem que você tenha vários runspaces gerenciados em segundo plano usando uma única variável.

Embora esse comportamento de vários runspaces possa ser feito com vários objetos runspace, usar um pool de runspace torna o gerenciamento muito mais fácil.

Os pools de runspace diferem dos runspaces individuais em como são configurados. Uma das principais diferenças é que você define a quantidade máxima de threads que podem ser usados para o pool de runspace. Com um runspace único, ele está limitado a um único thread, mas com um pool você especifica a quantidade máxima de threads que o pool pode escalar.

A quantidade recomendada de threads em um pool de runspace depende da quantidade de tarefas sendo executadas e da máquina em que o código está sendo executado. Embora aumentar a quantidade máxima de threads não afete negativamente a velocidade na maioria dos casos, você também pode não ver nenhum benefício.

Demonstração de Velocidade do Pool de Runspace

Para mostrar um exemplo de onde um grupo de runspace superará um único runspace, talvez você queira criar dez novos arquivos. Se você usar um único runspace para essa tarefa, você criaria o primeiro arquivo, depois passaria para o segundo e depois para o terceiro, e assim por diante, até criar os dez. O scriptblock para este exemplo pode parecer algo abaixo. Você alimentaria este scriptblock com dez nomes de arquivos em um loop e eles seriam todos criados.

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

No exemplo abaixo, um bloco de script é definido que contém um script curto que aceita um nome e cria um arquivo com esse nome. Um grupo de runspace é criado com um máximo de 5 threads.

Em seguida, um loop é executado dez vezes e, a cada vez, ele atribuirá o número da iteração a $_. Portanto, teria 1 na primeira iteração, 2 na segunda e assim por diante.

O loop cria um objeto PowerShell, atribui o bloco de script e o argumento para o script e inicia o processo.

Finalmente, no final do loop, ele aguardará a conclusão de todas as tarefas da fila.

$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
}

Agora, em vez de criar threads uma de cada vez, serão criadas cinco de uma vez. Sem grupos de runspace, você teria que criar e gerenciar cinco runspaces separados e cinco instâncias separadas de Powershell. Esse gerenciamento rapidamente se torna uma bagunça.

Em vez disso, você pode criar um grupo de runspace, uma instância de PowerShell, usar o mesmo bloco de código e o mesmo loop. A diferença é que o runspace dimensionará para usar todas as cinco dessas threads sozinho.

Criando Grupos de Runspace

A criação de um pool de espaços de execução é muito semelhante ao espaço de execução que foi criado em uma seção anterior. Abaixo está um exemplo de como fazer isso. A adição de um scriptblock e a invocação do processo é idêntica a um espaço de execução. Como você pode ver abaixo, o pool de espaços de execução está sendo criado com um máximo de cinco threads.

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

Comparando Espaços de Execução e Pools de Espaços de Execução para Velocidade

Para mostrar a diferença entre um espaço de execução e um pool de espaços de execução, crie um espaço de execução e execute o comando Start-Sleep novamente. Desta vez, no entanto, ele deve ser executado 10 vezes. Como você pode ver no código abaixo, está sendo criado um espaço de execução que vai dormir por 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}
}

Observe que, como você está usando um único espaço de execução, você terá que esperar até que ele seja concluído antes que outra invocação possa ser iniciada. É por isso que há um sleep de 100ms adicionado até que o trabalho seja concluído. Embora isso possa ser reduzido, você verá retornos decrescentes, pois estará gastando mais tempo verificando se o trabalho está concluído do que esperando que o trabalho termine.

A partir do exemplo abaixo, você pode ver que levou cerca de 51 segundos para completar 10 conjuntos de sleeps de 5 segundos.

Measuring performance of creating runspaces

Agora, em vez de usar um único espaço de execução, mude para um pool de espaços de execução. Abaixo está o código que será executado. Você pode ver que existem algumas diferenças entre o uso dos dois no código abaixo ao usar um pool de espaços de execução.

$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 você pode ver abaixo, isso é concluído em pouco mais de 10 segundos, o que é muito melhor do que os 51 segundos para o único espaço de execução.

Comparing runspaces vs. runspace pools

Abaixo está um resumo detalhado da diferença entre um espaço de execução e um pool de espaços de execução nesses exemplos.

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

Entrando no mundo dos Runspaces com 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.

A mesma coisa acontece com o PowerShell, onde algumas pessoas utilizam PSJobs em vez de runspaces devido à facilidade de uso. Existem algumas coisas que podem ser feitas para equilibrar as diferenças e obter um melhor desempenho sem tornar o uso muito mais difícil.

Existe um módulo amplamente utilizado chamado PoshRSJob que contém módulos que seguem o estilo dos PSJobs normais, mas com a vantagem adicional de usar runspaces. Em vez de ter que especificar todo o código para criar o runspace e o objeto do PowerShell, o módulo PoshRSJob cuida de fazer tudo isso quando você executa os comandos.

Para instalar o módulo, execute o comando abaixo em uma sessão do PowerShell com privilégios de administrador.

Install-Module PoshRSJob

Uma vez instalado o módulo, você pode ver que os comandos são os mesmos dos comandos PSJob, mas com um prefixo RS. Em vez de Start-Job, é Start-RSJob. Em vez de Get-Job, é Get-RSJob.

Abaixo está um exemplo de como executar o mesmo comando em um PSJob e depois em um RSJob. Como você pode ver, eles têm uma sintaxe e saída muito semelhantes, mas não são completamente idênticos.

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

Abaixo está um código que pode ser usado para comparar a diferença de velocidade entre um PSJob e um RSJob.

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

Como você pode ver abaixo, há uma grande diferença de velocidade, já que os RSJobs ainda estão usando runspaces internamente.

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

Foreach-Object -Parallel

A comunidade do PowerShell tem procurado por uma maneira mais fácil e integrada de processar várias threads de forma rápida. O comando paralelo é o resultado disso.

Até o momento em que escrevo isso, o PowerShell 7 ainda está em pré-visualização, mas eles adicionaram um parâmetro “Parallel” para o comando “Foreach-Object”. Esse processo utiliza runspaces para paralelizar o código e utiliza o scriptblock usado para o “Foreach-Object” como o scriptblock para o runspace.

Embora os detalhes ainda estejam sendo trabalhados, essa pode ser uma maneira mais fácil de usar runspaces no futuro. Como você pode ver abaixo, é possível percorrer rapidamente muitos conjuntos de “sleeps”.

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

Desafios com o Multi-Thread

Embora o multi-thread pareça incrível até agora, esse não é exatamente o caso. Existem muitos desafios que surgem ao lidar com multi-thread em qualquer código.

Uso de Variáveis

Um dos maiores e mais óbvios desafios do multi-thread é que você não pode compartilhar variáveis sem passá-las como argumentos. Existe uma exceção com um “synchronized hashtable”, mas isso é assunto para outro dia.

Tanto os PSJobs quanto os runspaces operam sem qualquer acesso a variáveis existentes e não há como interagir com as variáveis usadas em diferentes runspaces a partir do console.

Isso representa um grande desafio para a passagem dinâmica de informações para esses jobs. A resposta é diferente dependendo do tipo de multi-thread que você está usando.

Para Start-Job e Start-RSJob do módulo PoshRSJob, você pode usar o parâmetro ArgumentList para fornecer uma lista de objetos que serão passados como parâmetros para o scriptblock na ordem em que você os listar. Abaixo estão exemplos dos comandos usados para PSJobs e 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!"

Os runspaces nativos não oferecem a mesma facilidade. Em vez disso, você tem que usar o método AddArgument() no objeto PowerShell. Abaixo está um exemplo de como ficaria para cada um.

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()

Embora os pools de runspaces funcionem da mesma forma, abaixo está um exemplo de como adicionar um argumento a um pool 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

O multithreading também apresenta desafios de registro. Como cada thread está operando independentemente um do outro, todos não podem registrar no mesmo local. Se você tentar registrar em um arquivo com vários threads, sempre que um thread estiver gravando no arquivo, nenhum outro thread poderá fazê-lo. Isso pode retardar o seu código ou fazer com que ele falhe completamente.

Como exemplo, abaixo está um código para tentar registrar 100 vezes em um único arquivo usando 5 threads em um pool 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()

A partir da saída, você não verá erros, mas se você verificar o tamanho do arquivo de texto, poderá ver abaixo que nem todos os 100 trabalhos foram concluídos corretamente.

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

Algumas formas de contornar isso são registrar em arquivos separados. Isso remove o problema de bloqueio de arquivo, mas então você tem muitos arquivos de log pelos quais teria que procurar para descobrir tudo que aconteceu.

Outra alternativa é permitir que o tempo de saída de algumas das tarefas esteja incorreto e registrar apenas o que um trabalho fez uma vez que ele tenha terminado. Isso permite que tudo seja serializado por meio da sessão original, mas você perde alguns detalhes porque não sabe necessariamente em que ordem tudo ocorreu.

Resumo

Embora a multithreading possa fornecer grandes ganhos de desempenho, também pode causar dores de cabeça. Enquanto algumas cargas de trabalho se beneficiarão muito, outras podem não se beneficiar em nada. Existem muitos prós e contras em usar a multithreading, mas se usada corretamente, você pode reduzir drasticamente o tempo de execução do seu código.

Leitura adicional

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