Gravando o progresso em vários threads com Foreach Parallel

A partir do PowerShell 7.0, a capacidade de trabalhar em vários threads simultaneamente é possível usando o parâmetro Parallel no cmdlet Foreach-Object . No entanto, monitorar o progresso desses threads pode ser um desafio. Normalmente, você pode monitorar o progresso de um processo usando Write-Progress. No entanto, como o PowerShell usa um espaço de execução separado para cada thread ao usar o Parallel, relatar o progresso para o host não é tão simples quanto o uso normal do Write-Progress.

Usando uma hashtable sincronizada para acompanhar o progresso

Ao escrever o progresso de vários threads, o acompanhamento torna-se difícil porque, ao executar processos paralelos no PowerShell, cada processo tem seu próprio espaço de execução. Para contornar isso, você pode usar uma hashtable sincronizada. Uma hashtable sincronizada é uma estrutura de dados segura de thread que pode ser modificada por vários threads simultaneamente sem gerar um erro.

Configurar

Uma das desvantagens dessa abordagem é que ela precisa de uma configuração um pouco complexa para garantir que tudo seja executado sem erros.

$dataset = @(
    @{
        Id   = 1
        Wait = 3..10 | get-random | Foreach-Object {$_*100}
    }
    @{
        Id   = 2
        Wait = 3..10 | get-random | Foreach-Object {$_*100}
    }
    @{
        Id   = 3
        Wait = 3..10 | get-random | Foreach-Object {$_*100}
    }
    @{
        Id   = 4
        Wait = 3..10 | get-random | Foreach-Object {$_*100}
    }
    @{
        Id   = 5
        Wait = 3..10 | get-random | Foreach-Object {$_*100}
    }
)

# Create a hashtable for process.
# Keys should be ID's of the processes
$origin = @{}
$dataset | Foreach-Object {$origin.($_.id) = @{}}

# Create synced hashtable
$sync = [System.Collections.Hashtable]::Synchronized($origin)

Esta seção cria três estruturas de dados diferentes, para três finalidades diferentes.

A $dataSet variável armazena uma matriz de hashtables que é usada para coordenar as próximas etapas sem o risco de ser modificada. Se uma coleção de objetos for modificada durante a iteração pela coleção, o PowerShell lançará um erro. Você deve manter a coleção de objetos no loop separada dos objetos que estão sendo modificados. A Id chave em cada hashtable é o identificador de um processo simulado. A Wait chave simula a carga de trabalho de cada processo simulado que está sendo rastreado.

A $origin variável armazena uma hashtable aninhada com cada chave sendo uma das id's do processo fictício. Em seguida, ele é usado para hidratar a hashtable sincronizada armazenada na $sync variável. A $sync variável é responsável por relatar o progresso de volta para o espaço de execução pai, que exibe o progresso.

Executando os processos

Esta seção executa os processos multi-threaded e cria algumas das saídas usadas para exibir o progresso.

$job = $dataset | Foreach-Object -ThrottleLimit 3 -AsJob -Parallel {
    $syncCopy = $using:sync
    $process = $syncCopy.$($PSItem.Id)

    $process.Id = $PSItem.Id
    $process.Activity = "Id $($PSItem.Id) starting"
    $process.Status = "Processing"

    # Fake workload start up that takes x amount of time to complete
    start-sleep -Milliseconds ($PSItem.wait*5)

    # Process. update activity
    $process.Activity = "Id $($PSItem.id) processing"
    foreach ($percent in 1..100)
    {
        # Update process on status
        $process.Status = "Handling $percent/100"
        $process.PercentComplete = (($percent / 100) * 100)

        # Fake workload that takes x amount of time to complete
        Start-Sleep -Milliseconds $PSItem.Wait
    }

    # Mark process as completed
    $process.Completed = $true
}

Os processos simulados são enviados Foreach-Object e iniciados como trabalhos. O ThrottleLimit é definido como 3 para realçar a execução de vários processos em uma fila. Os trabalhos são armazenados na $job variável e nos permite saber quando todos os processos terminaram mais tarde.

Ao usar a using: instrução para fazer referência a uma variável de escopo pai no PowerShell, você não pode usar expressões para torná-la dinâmica. Por exemplo, se você tentasse criar a $process variável assim, $process = $using:sync.$($PSItem.id)obteria um erro informando que não pode usar expressões lá. Assim, criamos a $syncCopy variável para poder referenciar e modificar a $sync variável sem o risco de ela falhar.

Em seguida, construímos uma hashtable para representar o progresso do processo atualmente no loop usando a $process variável fazendo referência às chaves de hashtable sincronizadas. As chaves Activity e Statussão usadas como valores de parâmetro para Write-Progress exibir o status de um determinado processo simulado na próxima seção.

O foreach loop é apenas uma maneira de simular o processo funcionando e é randomizado com base no $dataSetatributo Wait para definir Start-Sleep usando milissegundos. A forma como calcula o progresso do seu processo pode variar.

Exibindo o progresso de vários processos

Agora que os processos fictícios estão sendo executados como trabalhos, podemos começar a gravar o progresso dos processos na janela do PowerShell.

while($job.State -eq 'Running')
{
    $sync.Keys | Foreach-Object {
        # If key is not defined, ignore
        if(![string]::IsNullOrEmpty($sync.$_.keys))
        {
            # Create parameter hashtable to splat
            $param = $sync.$_

            # Execute Write-Progress
            Write-Progress @param
        }
    }

    # Wait to refresh to not overload gui
    Start-Sleep -Seconds 0.1
}

A $job variável contém o trabalho pai e tem um trabalho filho para cada um dos processos simulados. Enquanto qualquer um dos trabalhos filho ainda estiver em execução, o Estado do trabalho pai permanecerá "Em execução". Isso nos permite usar o while loop para atualizar continuamente o progresso de cada processo até que todos os processos sejam concluídos.

Dentro do loop while, fazemos um loop através de cada uma das chaves na $sync variável. Uma vez que este é um hashtable sincronizado, é constantemente atualizado, mas ainda pode ser acessado sem lançar erros.

Há uma verificação para garantir que o processo que está sendo relatado está realmente sendo executado usando o IsNullOrEmpty() método. Se o processo não tiver sido iniciado, o loop não relatará e passará para o próximo até chegar a um processo que foi iniciado. Se o processo for iniciado, o hashtable da chave atual será usado para splat os parâmetros para Write-Progress.

Exemplo completo

# Example workload
$dataset = @(
    @{
        Id   = 1
        Wait = 3..10 | get-random | Foreach-Object {$_*100}
    }
    @{
        Id   = 2
        Wait = 3..10 | get-random | Foreach-Object {$_*100}
    }
    @{
        Id   = 3
        Wait = 3..10 | get-random | Foreach-Object {$_*100}
    }
    @{
        Id   = 4
        Wait = 3..10 | get-random | Foreach-Object {$_*100}
    }
    @{
        Id   = 5
        Wait = 3..10 | get-random | Foreach-Object {$_*100}
    }
)

# Create a hashtable for process.
# Keys should be ID's of the processes
$origin = @{}
$dataset | Foreach-Object {$origin.($_.id) = @{}}

# Create synced hashtable
$sync = [System.Collections.Hashtable]::Synchronized($origin)

$job = $dataset | Foreach-Object -ThrottleLimit 3 -AsJob -Parallel {
    $syncCopy = $using:sync
    $process = $syncCopy.$($PSItem.Id)

    $process.Id = $PSItem.Id
    $process.Activity = "Id $($PSItem.Id) starting"
    $process.Status = "Processing"

    # Fake workload start up that takes x amount of time to complete
    start-sleep -Milliseconds ($PSItem.wait*5)

    # Process. update activity
    $process.Activity = "Id $($PSItem.id) processing"
    foreach ($percent in 1..100)
    {
        # Update process on status
        $process.Status = "Handling $percent/100"
        $process.PercentComplete = (($percent / 100) * 100)

        # Fake workload that takes x amount of time to complete
        Start-Sleep -Milliseconds $PSItem.Wait
    }

    # Mark process as completed
    $process.Completed = $true
}

while($job.State -eq 'Running')
{
    $sync.Keys | Foreach-Object {
        # If key is not defined, ignore
        if(![string]::IsNullOrEmpty($sync.$_.keys))
        {
            # Create parameter hashtable to splat
            $param = $sync.$_

            # Execute Write-Progress
            Write-Progress @param
        }
    }

    # Wait to refresh to not overload gui
    Start-Sleep -Seconds 0.1
}