PowerShell Multithreading: Uma Investigação Profunda

Em algum momento, a maioria das pessoas se deparará com um problema em que um script básico do PowerShell é simplesmente muito lento para resolver. Isso pode envolver a coleta de dados de muitos computadores em sua rede ou talvez a criação de vários novos usuários no Active Directory de uma vez. Esses são ótimos exemplos de situações em que o uso de mais poder de processamento aceleraria a execução do seu código. Vamos ver como resolver isso usando multithreading no PowerShell!

A sessão padrão do PowerShell é de um único thread. Ele executa um comando e, quando termina, passa para o próximo comando. Isso é bom, pois mantém tudo repetível e não consome muitos recursos. Mas e se as ações que está realizando não dependerem uma da outra 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 utilizar várias técnicas de multithreading no PowerShell para processar vários fluxos de dados ao mesmo tempo, mas gerenciados pela mesma console.

Compreendendo o Multithreading no PowerShell

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

O principal benefício do multithreading é diminuir o tempo de execução do código. No entanto, essa redução de tempo vem com o custo de um requisito maior de potência de processamento. Ao usar multithreading, muitas ações são realizadas simultaneamente, exigindo assim 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 fazer várias tarefas ao mesmo tempo porque apenas um comando está sendo executado. Isso tudo muda quando você quer criar 1000 novos usuários.

Sem a capacidade de fazer várias tarefas ao mesmo tempo, você executaria o comando New-ADUser 1000 vezes para criar todos os usuários. Talvez levasse três segundos para criar um novo usuário. Para criar todos os 1000 usuários, levaria um pouco menos de uma hora. Em vez de usar uma tarefa para 1000 comandos, você poderia usar 100 tarefas, cada uma executando dez comandos. Agora, em vez de levar cerca de 50 minutos, você levaria menos de um minuto!

Note que você não verá escalabilidade perfeita. O ato de criar e destruir itens no código levará algum tempo. Usando uma única tarefa, o PowerShell precisa executar o código e pronto. Com várias tarefas, a tarefa original usada para executar o console será usada para gerenciar as outras tarefas. Em certo ponto, essa tarefa original será maximizada apenas para manter todas as outras tarefas em ordem.

Pré-requisitos para multithreading no PowerShell

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

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

Prioridade #1: Corrija seu código!

Antes de mergulhar na otimização de seus scripts com o multithreading do 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 consigo muita complexidade extra. Se houver maneiras de acelerar seu código antes do multithreading, elas devem ser feitas primeiro.

Identifique gargalos

Um dos primeiros passos para paralelizar seu código é descobrir o que está tornando-o lento. O código pode ser lento devido à lógica ruim ou a 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 uma grande quantidade de dados, qualquer filtro que você queira permitir para reduzir a quantidade de dados deve ser feito 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 filtrando um único processo (svchost). Em seguida, está selecionando a propriedade da CPU e garantindo 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 Get-Process retorne processos que não são relevantes.

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

Abaixo está a diferença de tempo para a execução das duas linhas acima. Embora a diferença de 117ms não seja perceptível se você executar este código apenas uma vez, ela começará a se acumular se for executada 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 à capacidade de um thread estar executando código enquanto outro thread está executando o mesmo código ao mesmo tempo sem causar conflitos.

Por exemplo, escrever no mesmo arquivo em dois threads diferentes não é thread-safe, pois não saberá o que adicionar ao arquivo primeiro. Enquanto dois threads lendo de um arquivo são thread-safe, já que o arquivo não está sendo alterado. Ambos os 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 aos threads coincidentemente acertarem o momento certo para não causar um conflito. Em outras ocasiões, você terá um conflito e isso tornará a solução do problema difícil devido aos erros inconsistentes.

Se você está executando apenas dois ou três trabalhos por vez, pode ser que eles coincidam de forma a escrever no arquivo em momentos diferentes. Então, quando você dimensiona o código para 20 ou 30 trabalhos, a probabilidade de pelo menos dois dos trabalhos tentarem escrever ao mesmo tempo diminui consideravelmente.

Execução Paralela com PSJobs

Uma das maneiras mais fáceis de criar um script multithread é usando PSJobs. PSJobs têm 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 neste 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 recuperados ou o trabalho pode ser removido.
  • Em Execução – O trabalho está em andamento e não pode ser removido sem interrompê-lo forçadamente. A saída também não pode ser recuperada ainda.
  • Bloqueado – O trabalho ainda está em execução, mas o host está sendo solicitado a fornecer informações antes que ele possa prosseguir.
  • Falha – Ocorreu um erro terminante durante a execução do trabalho.

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

Abaixo está a saída para 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 recuperado 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 terminou a execução. Você também pode ver que a propriedade HasMoreData é Falso. Isso significa que não houve saída para fornecer 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 o que pode ter causado a não conclusão de alguns desses trabalhos, como tentar aguardar por abc segundos resultou em um trabalho falhado.

All jobs

Criando Novos Trabalhos

Como você viu anteriormente, o comando Start-Job permite que você crie um novo trabalho que começa a executar código no trabalho. Ao criar um trabalho, você fornece um bloco de script que é usado para o trabalho. O PSJob então cria um trabalho com um número de ID único e inicia a execução do 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 foi capaz de executar o mesmo código em uma fração do tempo foi porque estava sendo executado em segundo plano como um PSJob. Levou 0,15 segundos para configurar e começar a executar o 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 uma 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, escreve a saída do trabalho no console. Qualquer coisa que tenha sido produzida pelo trabalho enquanto ele estava em execução foi armazenada, para que, quando o trabalho for recuperado, ele produza 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, 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 maneira 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 maneira 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 é inicializado 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 executará o trabalho agendado. Um trabalho agendado sem um gatilho precisa ser executado manualmente, mas cada trabalho pode ter muitos gatilhos.

Além de ter um gatilho, você ainda teria um bloco de script, assim como é usado com um PSJob normal. Depois de ter 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 sempre que alguém fizer login em um computador.

Para fazer isso, primeiro você definiria um gatilho usando New-JobTrigger e definiria o trabalho agendado como mostrado abaixo. Este trabalho agendado escreverá uma linha em um arquivo de log toda vez 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 pela captura de tela abaixo que registrou as tentativas.

Example login.log text file

Aproveitando o parâmetro AsJob

Outra maneira de usar trabalhos é usar o parâmetro AsJob que está integrado a 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 aguardarã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 de forma síncrona 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 o 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 utiliza o Invoke-Command para dormir por cinco segundos e então 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 Trabalhos, mas Mais Rápidos!

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

Runspaces são a área cercada em que o(s) thread(s) em execução do PowerShell operam. Enquanto o runspace usado com o console do PowerShell é restrito a um único 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, existem algumas grandes diferenças de desempenho. A maior diferença entre runspaces e PSJobs é o tempo necessário para configurar e desmontar cada um.

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

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

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

Measuring speed of job creation

Por outro lado, abaixo está o código utilizado 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 reduz 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 orientação de 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ê vai 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 novo runspace. Você faz isso usando a classe runspacefactory. Armazene isso em uma variável, como mostrado abaixo, para poder fazer referência posteriormente.

 $Runspace = [runspacefactory]::CreateRunspace()

Agora que o runspace foi criado, atribua-o a uma instância do PowerShell para executar o código do PowerShell. Para isso, você usará a classe powershell e, assim como o runspace, precisará armazená-la 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 espera 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ê vai querer 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 devolverá ao console.

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

$Job = $PowerShell.BeginInvoke()

Depois de ter a saída do BeginInvoke() armazenada em uma variável, você pode verificar essa variável para ver o status do trabalho, como visto 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 estiver concluído. Para fazer isso, você deve usar o método EndInvoke() uma vez que tenha sido concluído.

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

$PowerShell.EndInvoke($Job)

Assim 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. É aqui 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 próprio console do PowerShell e outro 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 únicos na forma como são configurados. Uma das principais diferenças é que você define a quantidade máxima de threads que podem ser usadas para o pool de runspace. Com um runspace único, ele é limitado a um único thread, mas com um pool, você especifica a quantidade máxima de threads que o pool pode aumentar.

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

Demonstração de Velocidade do Pool de Runspace

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

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

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

Em seguida, um loop se repete dez vezes e, a cada vez, atribuirá o número da iteração a $_. Assim, 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 esperará que todas as tarefas da fila terminem.

$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 de cada vez, ele criará cinco de uma vez. Sem pools de runspaces, 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 pool de runspaces, 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 automaticamente todos os cinco desses threads.

Criando Pools de Runspaces

A criação de um pool de runspaces é muito semelhante ao runspace que foi criado em uma seção anterior. Abaixo está um exemplo de como fazer isso. A adição de um scriptblock e o processo de invocação é idêntico a um runspace. Como você pode ver abaixo, o pool de runspaces 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 Runspaces e Pools de Runspaces para Velocidade

Para demonstrar a diferença entre um runspace e um pool de runspaces, crie um runspace e execute o comando Start-Sleep de antes. Desta vez, no entanto, ele deve ser executado 10 vezes. Como você pode ver no código abaixo, um runspace está sendo criado que irá 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 runspace, será necessário aguardar até que ele seja concluído antes que outra invocação possa ser iniciada. Por isso, há um intervalo de 100 ms 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á pronto do que esperando o trabalho terminar.

Do exemplo abaixo, você pode ver que levou cerca de 51 segundos para completar 10 conjuntos de 5 segundos de sono.

Measuring performance of creating runspaces

Agora, em vez de usar um único runspace, mude para um pool de runspaces. Abaixo está o código que será executado. Você pode ver que há algumas diferenças entre o uso dos dois no código abaixo ao usar um 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 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 runspace.

Comparing runspaces vs. runspace pools

Abaixo está um resumo detalhado da diferença entre um runspace e um pool de runspaces 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

Facilitando o uso de 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.

Isso também acontece com o PowerShell, onde algumas pessoas preferem usar PSJobs em vez de runspaces devido à facilidade de uso. Há algumas coisas que podem ser feitas para encontrar um meio-termo e obter um melhor desempenho sem torná-lo muito mais difícil de usar.

Existe um módulo amplamente utilizado chamado PoshRSJob que contém módulos que seguem o estilo dos PSJobs normais, mas com o benefício 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 como administrador.

Install-Module PoshRSJob

Uma vez que o módulo esteja instalado, você pode ver que os comandos são os mesmos que os comandos do PSJob, 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 novamente em um RSJob. Como você pode ver, eles têm uma sintaxe e saída muito similares, mas não são exatamente 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, existe uma grande diferença de velocidade, já que os RSJobs ainda estão usando runspaces por baixo dos panos.

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

Foreach-Object -Parallel

A comunidade do PowerShell tem desejado uma maneira mais fácil e integrada de realizar processos multithread de forma rápida. A opção de alternância paralela é o resultado desse desejo.

Ao escrever isso, o PowerShell 7 ainda está em prévia, mas eles adicionaram um parâmetro Parallel ao comando Foreach-Object. Esse processo utiliza runspaces para paralelizar o código e utiliza o scriptblock utilizado para o Foreach-Object como o scriptblock para o runspace.

Embora os detalhes ainda estejam sendo trabalhados, esta pode ser uma maneira mais fácil de usar runspaces no futuro. Como você pode ver abaixo, é possível percorrer rapidamente muitos 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

Desafios com Multithreading

Embora o multithreading pareça incrível até agora, essa não é exatamente a realidade. Existem muitos desafios ao realizar multithreading em qualquer código.

Uso de Variáveis

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

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

Isso representa um grande desafio para passar dinamicamente informações para esses trabalhos. A resposta é diferente dependendo do tipo de multithreading 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 lista. 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 espaços de execução 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 seria para cada um.

Espaço de execução:

$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 espaços de execução funcionem da mesma forma, abaixo está um exemplo de como adicionar um argumento a um pool de espaços de execução.

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

Logging

O multithreading também introduz desafios de logging. Como cada thread está operando independentemente uns dos outros, todos não podem fazer log no mesmo lugar. Se você tentasse fazer log em um arquivo com vários threads, sempre que um thread estivesse gravando no arquivo, nenhum outro thread poderia. Isso poderia diminuir a velocidade do seu código ou fazê-lo falhar completamente.

Como exemplo, abaixo está algum código para tentar fazer log 100 vezes em um único arquivo usando 5 threads em um pool de espaços de execução.

$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 olhar o tamanho do arquivo de texto, 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 maneiras de contornar isso é fazer log em arquivos separados. Isso remove o problema de bloqueio de arquivo, mas então você terá muitos arquivos de log pelos quais teria que passar para descobrir tudo o que aconteceu.

Outra alternativa é permitir que o temporizador de parte da saída esteja desativado, registrando apenas o que um trabalho fez uma vez concluído. Isso permite que tudo seja serializado por meio da sua sessão original, mas você perde alguns detalhes, pois não necessariamente sabe em que ordem tudo ocorreu.

Resumo

Embora a multithreading possa proporcionar enormes 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 no uso da 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/