Condividi tramite


Scrittura dello stato su più thread con Foreach Parallel

A partire da PowerShell 7.0, è possibile lavorare contemporaneamente su più thread usando il parametro Parallel nel cmdlet Foreach-Object. Il monitoraggio dello stato di avanzamento di questi thread può essere comunque complicato. In genere, è possibile monitorare lo stato di avanzamento di un processo usando Write-Progress. Tuttavia, poiché PowerShell usa un spazio di esecuzione separato per ogni thread quando si usa Parallel, la segnalazione dello stato di avanzamento all'host non è altrettanto semplice come l'uso normale di Write-Progress.

Uso di una tabella hash sincronizzata per monitorare lo stato di avanzamento

Quando si scrive lo stato di avanzamento da più thread, il monitoraggio diventa difficile perché durante l'esecuzione di processi paralleli in PowerShell ogni processo ha uno spazio di esecuzione proprio. Per aggirare questo problema, è possibile usare una tabella hash sincronizzata. Una tabella hash sincronizzata è una struttura di dati thread-safe che può essere modificata da più thread contemporaneamente senza generare un errore.

Impostazione

Uno degli svantaggi di questo approccio è che richiede una configurazione piuttosto complessa per garantire che l'esecuzione avvenga senza errori.

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

Questa sezione descrive come creare tre diverse strutture di dati per tre scopi diversi.

La variabile $dataSet archivia una matrice di tabelle hash usata per coordinare i passaggi successivi senza il rischio che i dati vengano modificati. Se una raccolta di oggetti viene modificata durante l'iterazione della raccolta, PowerShell genera un errore. È necessario mantenere la raccolta di oggetti nel ciclo separata dagli oggetti da modificare. La chiave Id in ogni tabella hash è l'identificatore per un processo fittizio. La chiave di Wait simula il carico di lavoro di ogni processo fittizio monitorato.

La variabile $origin archivia una tabella hash annidata in cui ogni chiave è uno degli ID del processo fittizio. Viene quindi usata per idratare la tabella hash sincronizzata archiviata nella variabile $sync. La variabile $sync è responsabile della segnalazione dello stato di avanzamento allo spazio di esecuzione padre, che lo visualizza.

Esecuzione dei processi

Questa sezione esegue i processi multithread e crea parte dell'output usato per visualizzare lo stato di avanzamento.

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

I processi fittizi vengono inviati a Foreach-Object e avviati come processi. ThrottleLimit è impostato su 3 per evidenziare l'esecuzione di più processi in una coda. I processi vengono archiviati nella variabile $job e consentono di stabilire quando tutti i processi sono stati completati in un secondo momento.

Quando si usa l'istruzione using: per fare riferimento a una variabile di ambito padre in PowerShell, non è possibile usare espressioni per renderla dinamica. Ad esempio, se si tentasse di creare la variabile $process in questo modo $process = $using:sync.$($PSItem.id), verrebbe visualizzato un errore per segnalare che non è possibile usare espressioni. La variabile $syncCopy viene quindi creata per poter fare riferimento e modificare la variabile $sync senza il rischio che si verifichi un errore.

Viene poi creata una tabella hash per rappresentare lo stato di avanzamento del processo attualmente nel ciclo usando la variabile $process tramite riferimento alle chiavi della tabella hash sincronizzata. Le chiavi Activity e Status vengono usate come valori di parametro per Write-Progress per visualizzare lo stato di un determinato processo fittizio nella sezione successiva.

Il ciclo foreach è semplicemente un modo per simulare il funzionamento del processo e viene eseguito in modo causale in base all'attributo Wait di $dataSet per impostare Start-Sleep usando i millisecondi. La modalità di calcolo dello stato di avanzamento del processo può variare.

Visualizzazione dello stato di avanzamento di più processi

Ora che i processi fittizi vengono eseguiti come processi, è possibile iniziare a scrivere lo stato di avanzamento dei processi nella finestra di 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
}

La variabile $job contiene il processo padre e ha un processo figlio per ognuno dei processi fittizi. Mentre i processi figlio sono ancora in esecuzione, State per il processo padre rimarrà "Running" (In esecuzione). In questo modo è possibile usare il ciclo while per aggiornare continuamente lo stato di ogni processo fino al termine di tutti i processi.

All'interno del ciclo while si scorre ogni chiave nella variabile $sync. Essendo sincronizzata, la tabella hash viene costantemente aggiornata, ma è comunque accessibile senza generare errori.

È previsto un controllo per verificare che il processo monitorato sia effettivamente in esecuzione usando il metodo IsNullOrEmpty(). Se il processo non è stato avviato, il ciclo non segnalerà lo stato e passerà al successivo fino a quando non rileva un processo avviato. Se il processo è avviato, la tabella hash dalla chiave corrente viene usata per passare i parametri a Write-Progress.

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