Sdílet prostřednictvím


Zaznamenávání postupu v několika vláknech pomocí ForEach-Object -Parallel

Počínaje PowerShellem 7.0 je možnost pracovat ve více vláknech současně pomocí parametru Parallel v rutině ForEach-Object. Monitorování průběhu těchto vláken ale může být náročné. Obvykle můžete sledovat průběh procesu pomocí Write-Progress. Vzhledem k tomu, že PowerShell používá pro každé vlákno při použití Parallelsamostatný runspace, zpráva o průběhu zpět k hostiteli není tak přímočaré jako běžné použití Write-Progress.

Sledování průběhu pomocí synchronizované hashovatelné tabulky

Při psaní průběhu z více vláken se sledování stává obtížné, protože při spouštění paralelních procesů v PowerShellu má každý proces vlastní prostředí runspace. K obejití tohoto problému můžete použít synchronizovanou hashovací tabulku . Synchronizovaná hashovatelná tabulka je datová struktura bezpečná pro vlákno, kterou lze upravovat více vlákny současně bez vyvolání chyby.

Nastavení

Jednou z nevýhod tohoto přístupu je, že vyžaduje poněkud složité nastavení, aby se zajistilo, že všechno běží bez chyby.

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

Tato část vytvoří tři různé datové struktury pro tři různé účely.

Proměnná $dataSet ukládá pole hashtables, které slouží ke koordinaci dalších kroků bez rizika změny. Pokud se během iterace v kolekci změní kolekce objektů, PowerShell vyvolá chybu. Kolekci objektů je nutné zachovat ve smyčce odděleně od upravovaných objektů. Klíč Id v každé hashovací tabulce je identifikátor simulovaného procesu. Klávesa Wait simuluje pracovní zátěž každého sledovaného simulovaného procesu.

Proměnná $origin ukládá vnořenou hashovací tabulku, kde každý klíč je jedním z falešných ID procesů. Pak se používá k hydrataci synchronizované hashovací tabulky uložené v $sync proměnné. Proměnná $sync zodpovídá za hlášení průběhu zpět do nadřazeného prostředí runspace, který zobrazuje průběh.

Spouštění procesů

Tato část spouští vícevláknové procesy a vytvoří některý výstup použitý k zobrazení průběhu.

$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
}

Simulované procesy se odesílají do ForEach-Object a spouštějí jako úlohy. ThrottleLimit je nastaven na 3 k zvýraznění spouštění více procesů ve frontě. Úlohy jsou uloženy v proměnné $job a umožňují nám zjistit, kdy všechny procesy byly dokončeny později.

Při použití příkazu Using: pro odkaz na proměnnou nadřazeného oboru v PowerShellu nemůžete výrazy použít k jeho dynamickému nastavení. Pokud jste se například pokusili vytvořit proměnnou $process takto, $process = $Using:sync.$($PSItem.Id), zobrazí se chyba s oznámením, že tam nemůžete použít výrazy. Vytvoříme tedy proměnnou $syncCopy, která bude schopna odkazovat na proměnnou $sync a upravit ji bez rizika, že selže.

Dále vytvoříme hashovací tabulku, která představuje průběh procesu aktuálně ve smyčce pomocí proměnné $process odkazováním na synchronizované hashovatelné klíče. aktivity a klíče Status se používají jako hodnoty parametrů pro Write-Progress k zobrazení stavu daného simulovaného procesu v další části.

Smyčka foreach je jen způsob, jak simulovat práci procesu a je náhodně generována na základě atributu Wait $dataSet, aby nastavila Start-Sleep pomocí milisekund. Způsob výpočtu průběhu procesu se může lišit.

Zobrazení průběhu více procesů

Teď, když jsou procesy napodobení spuštěné jako úlohy, můžeme začít psát průběh procesů do okna PowerShellu.

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
}

Proměnná $job obsahuje nadřazenou úlohu a pro každý ze simulačních procesů má podřízenou úlohu . Dokud je některá z podřízených úloh stále spuštěná, nadřazená úloha State zůstane spuštěná. Díky tomu můžeme pomocí smyčky while průběžně aktualizovat průběh každého procesu, dokud se všechny procesy nedokončí.

Ve smyčce while iterujeme přes jednotlivé klíče v proměnné $sync. Vzhledem k tomu, že se jedná o synchronizovanou hashovací tabulku, je neustále aktualizována, ale je stále přístupná bez vyvolání chyb.

Pomocí metody IsNullOrEmpty() zkontrolujete, že je nahlášený proces skutečně spuštěný. Pokud se proces nespustil, smyčka o něm neinformuje a přejde k dalšímu procesu, dokud se nedostane k procesu, který byl spuštěn. Pokud je proces spuštěn, hašovací tabulka z aktuálního klíče se použije k rozptýlení parametrů do Write-Progress.

Úplný příklad

# 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
}