Bem-vindo à aula 79 do nosso curso de PowerShell! Hoje vamos mergulhar em um tópico poderoso e muitas vezes mal compreendido: o paralelismo. Até agora, a maioria dos nossos scripts executou comandos de forma sequencial, um após o outro. Isso é simples e previsível, mas pode ser lento quando precisamos processar muitos itens, como arquivos, serviços ou chamadas de API. O paralelismo permite executar várias tarefas simultaneamente, aproveitando múltiplos núcleos da CPU e reduzindo drasticamente o tempo total de execução.

Nesta aula, vamos nos concentrar no cmdlet ForEach-Object -Parallel, introduzido no PowerShell 7, que é a maneira mais simples e acessível de adicionar paralelismo aos seus scripts. Também vamos explorar o conceito de runspaces (espaços de execução) para cenários mais avançados, entender o importante parâmetro ThrottleLimit e, crucialmente, discutir quando o paralelismo é realmente benéfico e quando pode causar mais problemas do que soluções.

ForEach-Object -Parallel (PS 7)

O cmdlet ForEach-Object sempre foi um pilar no processamento de coleções no PowerShell. Na versão 7, a Microsoft adicionou o parâmetro -Parallel, que permite que o bloco de script seja executado em paralelo para cada item da entrada. A sintaxe básica é:

1..10 | ForEach-Object -Parallel { $_ * 2 }

Neste exemplo, cada número de 1 a 10 é processado em paralelo, e o resultado é a multiplicação por 2. A variável $_ representa o item atual, assim como no modo sequencial. No entanto, há diferenças importantes: por padrão, o paralelismo usa runspaces separados, o que significa que o estado do script (variáveis, funções, etc.) não é compartilhado automaticamente. Por exemplo, se você definir uma variável fora do bloco, ela não estará disponível dentro dele, a menos que você use o parâmetro -ArgumentList ou a variável especial $using.

Vamos ver um exemplo mais prático. Suponha que você queira baixar vários arquivos de uma só vez. Em vez de usar Invoke-WebRequest em um loop sequencial, você pode fazer:

$urls = @("https://exemplo.com/arquivo1.zip", "https://exemplo.com/arquivo2.zip", "https://exemplo.com/arquivo3.zip")
$urls | ForEach-Object -Parallel {
    Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf)
}

Isso iniciará três downloads simultaneamente, o que pode ser muito mais rápido do que baixá-los um a um. No entanto, lembre-se de que cada runspace é independente, então você não pode simplesmente usar variáveis do escopo pai sem o mecanismo adequado.

Para passar valores para o script block, use -ArgumentList e o parâmetro $using dentro do bloco. Exemplo:

$destino = "C:\Downloads"
1..3 | ForEach-Object -Parallel {
    $item = $_
    $path = Join-Path $using:destino "arquivo$item.txt"
    # ... processamento ...
} -ArgumentList $destino

Na verdade, o $using é mais comum e direto. A sintaxe $using:variavel permite acessar variáveis do escopo do chamador. O -ArgumentList é útil quando você quer passar parâmetros posicionais, mas muitas vezes $using é suficiente.

Runspaces (visão geral)

Por baixo dos panos, o ForEach-Object -Parallel usa runspaces. Um runspace é essencialmente um ambiente de execução isolado do PowerShell, com seu próprio estado, variáveis, funções e módulos. Quando você usa -Parallel, o PowerShell cria um pool de runspaces e distribui os itens entre eles. Cada runspace executa o script block de forma independente.

Entender runspaces é fundamental para quem quer paralelismo mais avançado e controle fino. Com runspaces, você pode criar manualmente múltiplos runspaces, gerenciar sua sincronização e até mesmo compartilhar dados entre eles usando objetos como PSDataCollection e PowerShell .NET. No entanto, isso é muito mais complexo do que usar o cmdlet, e geralmente só é necessário para scripts muito sofisticados.

Um exemplo simples de criação manual de runspaces seria:

$ps = [PowerShell]::Create()
$ps.AddScript({ Start-Sleep 2; Get-Date })
$handle = $ps.BeginInvoke()
# ... fazer outras coisas ...
$result = $ps.EndInvoke($handle)
$ps.Dispose()

Mas isso é apenas um runspace. Para paralelismo real, você precisaria criar vários e gerenciá-los. Felizmente, o ForEach-Object -Parallel abstrai toda essa complexidade, então na maioria dos casos você não precisa se preocupar com runspaces diretamente. No entanto, é bom saber que eles existem e que o paralelismo do PowerShell é construído sobre eles.

Throttle

O parâmetro -ThrottleLimit controla quantos runspaces são criados simultaneamente, ou seja, quantos itens são processados em paralelo ao mesmo tempo. O valor padrão é 5, o que significa que no máximo 5 itens serão processados ao mesmo tempo, e os demais ficam em fila. Isso é importante para evitar sobrecarregar o sistema ou serviços externos (como APIs que têm limites de requisições).

Você pode ajustar esse valor conforme a necessidade. Por exemplo, se você tem 100 itens e seu computador tem 8 núcleos, talvez queira usar -ThrottleLimit 8 para aproveitar ao máximo a CPU. Mas se você está fazendo chamadas HTTP para um serviço que permite apenas 2 requisições simultâneas, defina -ThrottleLimit 2 para respeitar o limite.

Exemplo:

1..100 | ForEach-Object -Parallel { Start-Sleep -Milliseconds 500; $_ } -ThrottleLimit 10

Isso processará 10 itens por vez, com um atraso de 0,5 segundos cada, levando cerca de 5 segundos no total (100/10 * 0,5). Se não houvesse limite, todos os 100 seriam processados ao mesmo tempo, o que poderia causar problemas de recursos.

É importante encontrar um equilíbrio: um valor muito baixo não aproveita bem o paralelismo, enquanto um muito alto pode causar contenção de recursos e até erros. Uma boa prática é começar com o padrão e ajustar com base em testes e monitoramento.

Quando usar

O paralelismo não é uma solução mágica para tudo. Ele traz benefícios claros em cenários de I/O (entrada/saída), como downloads, leitura de arquivos, consultas a bancos de dados ou chamadas a APIs, onde o tempo de espera é dominante. Nesses casos, a CPU fica ociosa enquanto aguarda a resposta, e o paralelismo permite que outras tarefas aproveitem esse tempo.

Por outro lado, se suas tarefas são intensivas em CPU (como cálculos complexos), o paralelismo pode ajudar, mas apenas se houver múltiplos núcleos disponíveis. Em uma máquina com um único núcleo, o paralelismo pode até piorar o desempenho devido à troca de contexto.

Além disso, evite usar paralelismo quando a ordem dos resultados importa ou quando as tarefas compartilham estado (como escrever no mesmo arquivo). O paralelismo não garante ordem de conclusão, e você precisará implementar sincronização, o que adiciona complexidade.

Outro ponto: o ForEach-Object -Parallel não está disponível no Windows PowerShell 5.1, apenas no PowerShell 7+. Se você precisa manter compatibilidade com versões antigas, terá que usar runspaces manualmente ou outras técnicas, como Start-Job (que é mais pesado).

Em resumo, use paralelismo quando:

  • Você tem muitas tarefas independentes.
  • O tempo de execução é dominado por espera (I/O).
  • Você tem múltiplos núcleos de CPU disponíveis.
  • A ordem de processamento não é crítica.
  • Você pode tolerar alguma complexidade adicional no código.

Boas práticas e observações finais

Aqui estão algumas dicas para usar paralelismo com segurança:

  • Use $using: para acessar variáveis do escopo pai dentro do bloco paralelo.
  • Evite modificar variáveis do escopo pai dentro do bloco; em vez disso, colete a saída e processe depois.
  • Se precisar gravar em arquivos, use nomes únicos ou um mecanismo de bloqueio (por exemplo, lock ou Semaphore).
  • Teste com um pequeno conjunto de dados antes de executar em grande escala para ajustar o ThrottleLimit.
  • Monitore o uso de CPU e memória para garantir que você não está sobrecarregando o sistema.

O paralelismo é uma ferramenta valiosa no arsenal do profissional de PowerShell, mas deve ser usada com discernimento. Com o ForEach-Object -Parallel, você pode acelerar significativamente scripts de processamento em lote, tornando suas automações mais eficientes e responsivas.

Referências

Exercícios

  1. Exercício 1: Escreva um comando que use ForEach-Object -Parallel para calcular o quadrado de cada número de 1 a 10 e exiba o resultado.
  2. ✓ Resposta:
    1..10 | ForEach-Object -Parallel { $_ * $_ }
  3. Exercício 2: Crie uma lista de URLs e baixe-as em paralelo, salvando cada arquivo com o nome base da URL. Use um ThrottleLimit de 3.
  4. ✓ Resposta:
    $urls = @("https://exemplo.com/a.zip", "https://exemplo.com/b.zip", "https://exemplo.com/c.zip")
    $urls | ForEach-Object -Parallel { Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf) } -ThrottleLimit 3
  5. Exercício 3: Explique por que a variável $nome definida fora do bloco não é acessível dentro de ForEach-Object -Parallel e como você pode torná-la acessível.
  6. ✓ Resposta: Porque cada runspace tem seu próprio escopo e não herda automaticamente as variáveis do escopo do chamador. Para acessá-la, use $using:nome dentro do bloco.
  7. Exercício 4: Escreva um script que processe 100 itens com um ThrottleLimit de 10 e meça o tempo de execução comparando com a versão sequencial (use Measure-Command).
  8. ✓ Resposta:
    $itens = 1..100
    $tempoParalelo = Measure-Command { $itens | ForEach-Object -Parallel { Start-Sleep -Milliseconds 100; $_ } -ThrottleLimit 10 }
    $tempoSequencial = Measure-Command { $itens | ForEach-Object { Start-Sleep -Milliseconds 100; $_ } }
    "Paralelo: $($tempoParalelo.TotalSeconds)s"
    "Sequencial: $($tempoSequencial.TotalSeconds)s"
  9. Exercício 5: Liste três situações em que você NÃO deve usar paralelismo e explique o motivo.
  10. ✓ Resposta: 1) Quando a ordem dos resultados é importante, pois a conclusão é não determinística. 2) Quando há dependência entre tarefas (uma precisa do resultado da outra). 3) Quando há recursos limitados (como uma API com limite de requisições) e você pode sobrecarregá-los.