Runspaces são o mecanismo fundamental de execução paralela no PowerShell. Enquanto a maioria dos cmdlets e scripts executam de forma sequencial, runspaces permitem executar blocos de código simultaneamente, aproveitando múltiplos núcleos da CPU. Compreender runspaces é essencial para otimizar scripts que processam grandes volumes de dados ou que realizam operações independentes.

Nesta aula, vamos mergulhar no conceito de runspaces, explorar como eles fornecem paralelismo de baixo nível, compará-los com jobs tradicionais e identificar cenários práticos onde seu uso é vantajoso. Você aprenderá a criar e gerenciar runspaces, além de boas práticas para evitar erros comuns.

O que são

Um runspace é o ambiente de execução onde um comando ou script do PowerShell é executado. Cada runspace possui seu próprio estado, incluindo variáveis, funções e módulos carregados. Por padrão, quando você executa um comando no console, ele roda em um runspace único. Para executar tarefas em paralelo, você pode criar múltiplos runspaces, cada um com seu próprio pipeline e estado.

Os runspaces são criados usando a classe System.Management.Automation.Runspaces.Runspace ou, mais comumente, com o comando CreateRunspace() da classe PowerShell. Você pode abrir um runspace, executar um script e fechá-lo. Para comunicação entre o runspace principal e os filhos, são usados objetos como RunspacePool e PowerShell.

# Criando e usando um runspace simples
$runspace = [runspacefactory]::CreateRunspace()
$runspace.Open()
$ps = [powershell]::Create()
$ps.Runspace = $runspace
$ps.AddScript({ "Executando no runspace: $PID" })
$result = $ps.Invoke()
$result
$ps.Dispose()
$runspace.Close()

Neste exemplo, criamos um runspace, abrimos, associamos a uma instância de PowerShell e invocamos um script simples. O resultado é exibido no console. Note que o runspace é independente do runspace principal, então o PID pode ser diferente.

Paralelismo de baixo nível

Runspaces oferecem paralelismo de baixo nível, o que significa que eles operam diretamente com threads do sistema operacional. Quando você cria múltiplos runspaces, eles podem ser executados em threads separados, permitindo que o sistema operacional distribua o trabalho entre núcleos de CPU. Isso é diferente de técnicas de paralelismo de alto nível, como jobs, que envolvem processos separados e comunicação via serialização.

O paralelismo de baixo nível é mais eficiente porque não há overhead de criação de processos completos. No entanto, exige cuidado com concorrência, pois múltiplos threads podem acessar os mesmos recursos simultaneamente. Para gerenciar isso, o PowerShell oferece RunspacePool, que permite limitar o número de runspaces ativos e compartilhar dados de forma controlada.

# Usando RunspacePool para controlar concorrência
$pool = [runspacefactory]::CreateRunspacePool(1, 4) # mínimo 1, máximo 4 runspaces
$pool.Open()
$scripts = 1..10
$psList = foreach ($i in $scripts) {
    $ps = [powershell]::Create()
    $ps.RunspacePool = $pool
    $ps.AddScript("Start-Sleep -Milliseconds 100; Resultado $i")
    $ps
}
$results = $psList | ForEach-Object { $_.BeginInvoke() }
# Aguardando e coletando resultados
$output = foreach ($r in $results) {
    $r.AsyncWaitHandle.WaitOne()
    $_.EndInvoke($r)
}
$output
$psList | ForEach-Object { $_.Dispose() }
$pool.Close()

Este exemplo cria um pool com até 4 runspaces ativos e executa 10 scripts. O método BeginInvoke inicia a execução assíncrona, e EndInvoke coleta os resultados. Isso permite processamento paralelo com limite de threads, evitando sobrecarga do sistema.

vs jobs

Jobs no PowerShell (como Start-Job) são uma abstração de mais alto nível para executar tarefas em segundo plano. Cada job é executado em um processo separado, o que fornece isolamento total, mas com custo de performance significativo. Jobs são ideais para tarefas de longa duração ou que precisam de isolamento, mas não para paralelismo intensivo.

Por outro lado, runspaces são mais leves e rápidos, pois compartilham o mesmo processo. Eles são adequados para tarefas que exigem alta performance e onde o overhead de processos seria proibitivo. No entanto, runspaces não oferecem o mesmo nível de isolamento: se um runspace falhar, pode afetar outros. A tabela abaixo resume as diferenças:

CaracterísticaRunspacesJobs
ExecuçãoThreads no mesmo processoProcessos separados
PerformanceAlta (baixo overhead)Baixa (overhead de processo)
IsolamentoParcial (mesmo processo)Total (processo separado)
ComunicaçãoDireta via objetos .NETVia serialização (remoting)
Uso típicoProcessamento paralelo intensivoTarefas em background simples

Em termos práticos, se você precisa processar milhares de itens rapidamente, runspaces são a escolha certa. Se você quer simplesmente executar um script em segundo plano sem bloquear o console, jobs são mais simples.

Casos de uso

Runspaces são particularmente úteis em cenários onde a paralelização pode reduzir drasticamente o tempo de execução. Alguns exemplos:

  • Processamento de coleções: iterar sobre milhares de arquivos ou objetos e realizar operações independentes.
  • Consultas a serviços externos: fazer múltiplas chamadas HTTP ou consultas a bancos de dados simultaneamente.
  • Análise de dados: dividir grandes conjuntos de dados em partes e processá-las em paralelo.
  • Scripts de automação: executar várias tarefas de administração do sistema ao mesmo tempo.

Por exemplo, para testar conectividade a vários servidores, você pode usar runspaces para executar Test-Connection em paralelo:

$servers = "server1", "server2", "server3", "server4"
$pool = [runspacefactory]::CreateRunspacePool(1, 4)
$pool.Open()
$psList = foreach ($server in $servers) {
    $ps = [powershell]::Create()
    $ps.RunspacePool = $pool
    $ps.AddScript("Test-Connection -ComputerName $server -Count 1 -Quiet")
    [PSCustomObject]@{ Server = $server; PS = $ps }
}
$results = foreach ($item in $psList) {
    $handle = $item.PS.BeginInvoke()
    [PSCustomObject]@{ Server = $item.Server; Handle = $handle }
}
foreach ($r in $results) {
    $r.Handle.AsyncWaitHandle.WaitOne()
    $output = $r.PS.EndInvoke($r.Handle)
    "$($r.Server): $output"
}
$psList | ForEach-Object { $_.PS.Dispose() }
$pool.Close()

Isso testa todos os servidores simultaneamente, reduzindo o tempo total de execução.

Boas práticas

Ao trabalhar com runspaces, é importante seguir algumas boas práticas:

  • Limitar o número de runspaces: use RunspacePool para evitar criar threads demais, o que pode causar contenção e degradação de desempenho.
  • Gerenciar o ciclo de vida: sempre feche e descarte runspaces e instâncias de PowerShell para liberar recursos.
  • Evitar compartilhamento de estado: cada runspace tem seu próprio estado; para compartilhar dados, use variáveis de pool ou objetos sincronizados.
  • Tratar exceções: dentro dos scripts executados em runspaces, capture exceções para evitar que o runspace termine abruptamente.

Com essas práticas, você pode aproveitar ao máximo o paralelismo de baixo nível.

Referências

Exercícios

  1. Crie um runspace simples que execute o comando Get-Process e exiba a contagem de processos.

    ✓ Resposta:
    $rs = [runspacefactory]::CreateRunspace()
    $rs.Open()
    $ps = [powershell]::Create()
    $ps.Runspace = $rs
    $ps.AddScript({ (Get-Process).Count })
    $result = $ps.Invoke()
    $result
    $ps.Dispose()
    $rs.Close()
  2. Explique a diferença entre runspaces e jobs em termos de isolamento e performance.

    ✓ Resposta: Runspaces executam em threads dentro do mesmo processo, oferecendo maior performance e menor overhead, mas com isolamento parcial. Jobs executam em processos separados, fornecendo isolamento total, porém com custo de performance devido à criação de processos e serialização de dados.
  3. Usando um RunspacePool, execute 5 scripts que façam Start-Sleep -Milliseconds 500 e retornem uma mensagem, com no máximo 2 runspaces ativos. Colete e exiba os resultados.

    ✓ Resposta:
    $pool = [runspacefactory]::CreateRunspacePool(1, 2)
    $pool.Open()
    $psList = 1..5 | ForEach-Object {
        $ps = [powershell]::Create()
        $ps.RunspacePool = $pool
        $ps.AddScript("Start-Sleep -Milliseconds 500; 'Tarefa $_ concluída'")
        $ps
    }
    $handles = $psList | ForEach-Object { $_.BeginInvoke() }
    $results = foreach ($i in 0..4) {
        $handles[$i].AsyncWaitHandle.WaitOne()
        $psList[$i].EndInvoke($handles[$i])
    }
    $results
    $psList | ForEach-Object { $_.Dispose() }
    $pool.Close()
  4. Escreva um script que use runspaces para calcular o quadrado de números de 1 a 10 em paralelo e exiba os resultados.

    ✓ Resposta:
    $pool = [runspacefactory]::CreateRunspacePool(1, 4)
    $pool.Open()
    $psList = 1..10 | ForEach-Object {
        $n = $_
        $ps = [powershell]::Create()
        $ps.RunspacePool = $pool
        $ps.AddScript("param(`$n) `$n * `$n").AddArgument($n)
        $ps
    }
    $handles = $psList | ForEach-Object { $_.BeginInvoke() }
    $results = for ($i = 0; $i -lt 10; $i++) {
        $handles[$i].AsyncWaitHandle.WaitOne()
        $psList[$i].EndInvoke($handles[$i])
    }
    $results
    $psList | ForEach-Object { $_.Dispose() }
    $pool.Close()
  5. Cite três casos de uso onde runspaces são mais adequados que jobs e explique o porquê.

    ✓ Resposta: 1) Processamento de grandes coleções de dados, pois a performance é crítica e o overhead de processos seria proibitivo. 2) Chamadas HTTP em massa, onde a latência de rede domina e runspaces permitem muitas requisições simultâneas com baixo custo. 3) Scripts de automação que precisam executar várias tarefas independentes rapidamente, como verificação de status de serviços em muitos servidores.