Paralelismo
LidoEsta aula explora paralelismo em PowerShell, focando no cmdlet ForEach-Object -Parallel do PowerShell 7, incluindo a sintaxe, o parâmetro ThrottleLimit e boas práticas. Também oferece uma visão geral de runspaces para cenários mais avançados e discute quando usar paralelismo versus execução sequencial.
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 $destinoNa 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 10Isso 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,
lockouSemaphore). - 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
- Documentação oficial do ForEach-Object
- Tudo sobre paralelismo no PowerShell (blog oficial)
- Usando processamento paralelo no PowerShell
- Anúncio oficial do PowerShell 7 com paralelismo
- Classe Runspace (documentação .NET)
- Sobre variáveis remotas ($using)
Exercícios
- Exercício 1: Escreva um comando que use
ForEach-Object -Parallelpara calcular o quadrado de cada número de 1 a 10 e exiba o resultado. - 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.
- Exercício 3: Explique por que a variável
$nomedefinida fora do bloco não é acessível dentro deForEach-Object -Parallele como você pode torná-la acessível. - 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). - Exercício 5: Liste três situações em que você NÃO deve usar paralelismo e explique o motivo.
1..10 | ForEach-Object -Parallel { $_ * $_ }$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$using:nome dentro do bloco.$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"