Udostępnij za pomocą


Śledzenie postępu w wielu wątkach za pomocą ForEach-Object -Parallel

Począwszy od programu PowerShell 7.0, możliwość pracy w wielu wątkach jednocześnie jest możliwa przy użyciu parametru Parallel w ForEach-Object cmdlet. Monitorowanie postępu tych wątków może być jednak wyzwaniem. Zwykle można monitorować postęp procesu przy użyciu write-progress. Jednak ponieważ program PowerShell używa oddzielnego obszaru roboczego (runspace) dla każdego wątku podczas korzystania z Parallel, przekazywanie postępu z powrotem do hosta nie jest tak bezpośrednie, jak normalne użycie Write-Progress.

Śledzenie postępu za pomocą zsynchronizowanej tabeli skrótów

Podczas pisania postępu z wielu wątków śledzenie staje się trudne, ponieważ podczas uruchamiania równoległych procesów w programie PowerShell każdy proces ma własną przestrzeń uruchomieniową. Aby obejść ten proces, możesz użyć zsynchronizowanej tabeli skrótów. Zsynchronizowana tabela skrótów to struktura danych bezpieczna dla wątków, którą można modyfikować jednocześnie przez wiele wątków bez zgłaszania błędu.

Ustawienia

Jedną z wad tego podejścia jest to, że potrzeba nieco złożonej konfiguracji, aby upewnić się, że wszystko działa bez błędów.

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

Ta sekcja tworzy trzy różne struktury danych na potrzeby trzech różnych celów.

Zmienna $dataSet przechowuje tablicę tabel skrótów, która służy do koordynowania kolejnych kroków, bez ryzyka ich zmiany. Jeśli kolekcja obiektów zostanie zmodyfikowana podczas iteracji w kolekcji, program PowerShell zgłasza błąd. Należy zachować kolekcję obiektów w pętli oddzieloną od modyfikowanych obiektów. Klucz Id w każdej tabeli skrótu jest identyfikatorem pozornego procesu. Klucz Wait symuluje obciążenie każdego śledzonego procesu symulowanego.

Zmienna $origin przechowuje zagnieżdżoną tabelę mieszającą, gdzie każdy klucz to jeden z identyfikatorów testowych procesu. Następnie służy do nawilżania zsynchronizowanej tabeli skrótowej przechowywanej w zmiennej $sync. Zmienna $sync jest odpowiedzialna za raportowanie postępu z powrotem do przestrzeni uruchomieniowej nadrzędnej, która wyświetla postęp.

Uruchamianie procesów

Ta sekcja uruchamia wielowątkowy proces i tworzy niektóre dane wyjściowe używane do wyświetlania postępu.

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

Symulowane procesy są wysyłane do ForEach-Object i uruchamiane jako zadania. ThrottleLimit jest ustawiony na 3, aby podkreślić uruchamianie wielu procesów w kolejce. Zadania są przechowywane w zmiennej $job i umożliwiają nam poznanie, kiedy wszystkie procesy zakończyły się później.

Podczas używania instrukcji Using: do odwoływania się do zmiennej zakresu nadrzędnego w PowerShell nie można używać wyrażeń w celu dynamicznego jej określania. Jeśli na przykład podjęto próbę utworzenia zmiennej $process w następujący sposób, $process = $Using:sync.$($PSItem.Id)zostanie wyświetlony błąd z informacją, że nie można tam używać wyrażeń. Dlatego tworzymy zmienną $syncCopy, aby móc odwoływać się do zmiennej $sync i modyfikować ją bez ryzyka wystąpienia błędu.

Następnie rozbudujemy tabelę skrótów, aby reprezentowała postęp procesu aktualnie w pętli, używając zmiennej $process i odwołując się do zsynchronizowanych kluczy tabeli skrótów. Activity i Status są używane jako wartości parametrów dla Write-Progress do wyświetlania statusu danego symulowanego procesu w następnej sekcji.

Pętla foreach to tylko sposób na symulację działania procesu i jest losowana na podstawie atrybutu $dataSetWait do ustawienia Start-Sleep przy użyciu milisekund. Sposób obliczania postępu procesu może się różnić.

Wyświetlanie postępu wielu procesów

Teraz, gdy pozorne procesy działają jako zadania, możemy rozpocząć zapisywanie postępu procesów w oknie programu 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
}

Zmienna $job zawiera zadanie nadrzędne i ma podrzędne zadanie dla każdego z procesów pozornych. Podczas gdy którekolwiek z zadań podrzędnych jest nadal uruchomione, zadanie nadrzędne State pozostanie "Uruchomione". Dzięki temu możemy używać pętli while, aby stale aktualizować postęp każdego procesu do momentu zakończenia wszystkich procesów.

W pętli while przechodzimy przez poszczególne klucze w zmiennej $sync. Ponieważ jest to zsynchronizowana tabela skrótów, jest stale aktualizowana, ale nadal można z niej korzystać bez wystąpienia błędów.

Istnieje kontrola, aby upewnić się, że zgłaszany proces jest rzeczywiście uruchomiony przy użyciu metody IsNullOrEmpty(). Jeśli proces nie został uruchomiony, pętla nie będzie tego raportować i nie przejdzie do kolejnego procesu, dopóki nie trafi na proces, który został uruchomiony. Jeśli proces zostanie uruchomiony, tabela skrótu z bieżącego klucza zostanie użyta do przekazania parametrów do Write-Progress.

Pełny przykład

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