Dela via


Skriva förlopp över flera trådar med ForEach-Object -Parallel

Från och med PowerShell 7.0 är möjligheten att arbeta i flera trådar samtidigt möjlig med hjälp av parametern Parallel i cmdleten ForEach-Object . Att övervaka förloppet för dessa trådar kan dock vara en utmaning. Normalt kan du övervaka förloppet för en process med hjälp av Write-Progress. Men eftersom PowerShell använder ett separat körningsutrymme för varje tråd när du använder Parallel, är det inte lika enkelt att rapportera förloppet tillbaka till värden som den normala användningen av Write-Progress.

Använda en synkroniserad hashtable för att spåra förloppet

När du skriver förloppet från flera trådar blir det svårt att spåra, eftersom varje process har en egen runspace när parallella processer körs i PowerShell. För att komma runt detta kan du använda en synkroniserad hashtable. En synkroniserad hashtable är en trådsäker datastruktur som kan ändras av flera trådar samtidigt utan att utlösa ett fel.

Ställ in

En av nackdelarna med den här metoden är att det krävs en något komplex konfiguration för att säkerställa att allt körs utan fel.

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

Det här avsnittet skapar tre olika datastrukturer för tre olika syften.

Variabeln $dataSet lagrar en matris med hashtables som används för att samordna nästa steg utan risk för att ändras. Om en objektsamling ändras när den itererar genom samlingen genererar PowerShell ett fel. Du måste hålla objektsamlingen i loopen separat från de objekt som ändras. Nyckeln Id i varje hashtable är identifieraren för en modellprocess. Nyckeln Wait simulerar arbetsbelastningen för varje modellprocess som spåras.

Variabeln $origin lagrar en kapslad hashtable där varje nyckel är ett av de falska process-ID:na. Sedan används den för att hydratisera den synkroniserade hashtable som lagras i variabeln $sync . Variabeln $sync ansvarar för att rapportera förloppet tillbaka till det överordnade runspacet, som visar förloppet.

Köra processerna

Det här avsnittet kör processerna med flera trådar och skapar några av de utdata som används för att visa förloppet.

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

Mock-processerna skickas till ForEach-Object och startas som jobb. ThrottleLimit är inställt på 3 för att markera körning av flera processer i en kö. Jobben lagras i variabeln $job och gör att vi kan veta när alla processer har slutförts senare.

När du använder -instruktionen Using: för att referera till en överordnad omfångsvariabel i PowerShell kan du inte använda uttryck för att göra den dynamisk. Om du till exempel försökte skapa variabeln $process så här $process = $Using:sync.$($PSItem.Id)får du ett felmeddelande om att du inte kan använda uttryck där. Därför skapar vi variabeln $syncCopy för att kunna referera till och ändra variabeln $sync utan risk för att den misslyckas.

Sedan skapar vi en hashtable för att representera förloppet för den process som för närvarande finns i loopen med hjälp av variabeln $process genom att referera till de synkroniserade hashtable-nycklarna. Nycklarna Aktivitet och Status används som parametervärden för Write-Progress att visa status för en viss mockprocess i nästa avsnitt.

Loopen foreach är bara ett sätt att simulera att processen fungerar och randomiseras baserat på $dataSet attributet Vänta som ska anges Start-Sleep med millisekunder. Hur du beräknar processens förlopp kan variera.

Visa förloppet för flera processer

Nu när mockprocesserna körs som jobb kan vi börja skriva processernas förlopp till PowerShell-fönstret.

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
}

Variabeln $job innehåller det överordnade jobbet och har ett underordnat jobb för var och en av de falska processerna. Medan något av de underordnade jobben fortfarande körs förblir det överordnade jobbet State "Körs". På så sätt kan vi använda loopen while för att kontinuerligt uppdatera förloppet för varje process tills alla processer är klara.

I while-loopen loopar vi igenom var och en av nycklarna i variabeln $sync . Eftersom detta är en synkroniserad hashtable uppdateras den ständigt men kan fortfarande nås utan att utlösa några fel.

Det finns en kontroll för att säkerställa att den process som rapporteras faktiskt körs med hjälp av IsNullOrEmpty() metoden. Om processen inte har startats rapporterar inte loopen om den och går vidare till nästa tills den kommer till en process som har startats. Om processen startas används hashtabellen från den aktuella nyckeln för att splat parametrarna till Write-Progress.

Fullständigt exempel

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