Bagikan melalui


Menulis Kemajuan di beberapa utas dengan ForEach-Object -Parallel

Mulai dari PowerShell 7.0, kemampuan untuk bekerja di beberapa utas secara bersamaan dimungkinkan menggunakan parameter Paralel di cmdlet ForEach-Object . Memantau kemajuan utas ini bisa menjadi tantangan sekalipun. Biasanya, Anda dapat memantau kemajuan proses menggunakan Write-Progress. Namun, karena PowerShell menggunakan runspace terpisah untuk setiap utas saat menggunakan Parallel, melaporkan kemajuan kembali ke host tidak semudah penggunaan Write-Progress normal.

Menggunakan hashtable yang disinkronkan untuk melacak kemajuan

Saat menulis kemajuan dari beberapa utas, pelacakan menjadi sulit karena saat menjalankan proses paralel di PowerShell, setiap proses memiliki runspacenya sendiri. Untuk mengatasi hal ini, Anda dapat menggunakan hashtable yang disinkronkan. Hashtable yang disinkronkan adalah struktur data utas aman yang dapat dimodifikasi oleh beberapa utas secara bersamaan tanpa mengakibatkan kesalahan.

Pengaturan

Salah satu kelemahan dari pendekatan ini adalah dibutuhkan, pengaturan yang agak kompleks untuk memastikan semuanya berjalan tanpa kesalahan.

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

Bagian ini membuat tiga struktur data yang berbeda, untuk tiga tujuan yang berbeda.

Variabel $dataSet menyimpan array hashtable yang digunakan untuk mengoordinasikan langkah berikutnya tanpa risiko dimodifikasi. Jika koleksi objek dimodifikasi saat melakukan iterasi melalui koleksi, PowerShell akan menampilkan kesalahan. Anda harus menjaga koleksi objek dalam perulangan terpisah dari objek yang sedang dimodifikasi. Kunci Id di setiap hashtable adalah pengidentifikasi untuk proses tiruan. Kunci Wait mensimulasikan beban kerja setiap proses tiruan yang sedang dilacak.

Variabel $origin menyimpan hashtable berlapis dengan setiap kunci menjadi salah satu id proses tiruan. Kemudian, ini digunakan untuk menghidrasi hashtable yang disinkronkan dan disimpan dalam variabel $sync. Variabel $sync bertanggung jawab untuk melaporkan kemajuan kembali ke runspace induk, yang kemudian menampilkan kemajuan tersebut.

Menjalankan proses-proses

Bagian ini menjalankan proses multi-utas dan membuat beberapa output yang digunakan untuk menampilkan kemajuan.

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

Proses tiruan dikirim ke ForEach-Object dan dimulai sebagai tugas. ThrottleLimit diatur ke 3 untuk menyoroti menjalankan beberapa proses dalam antrean. Pekerjaan disimpan dalam $job variabel dan memungkinkan kita untuk mengetahui kapan semua proses telah selesai nanti.

Saat menggunakan Using: pernyataan untuk mereferensikan variabel cakupan induk di PowerShell, Anda tidak dapat menggunakan ekspresi untuk membuatnya dinamis. Misalnya, jika Anda mencoba membuat $process variabel seperti ini, $process = $Using:sync.$($PSItem.Id), Anda akan mendapatkan kesalahan yang menyatakan Bahwa Anda tidak dapat menggunakan ekspresi di sana. Jadi, kita membuat $syncCopy variabel untuk dapat mereferensikan dan memodifikasi $sync variabel tanpa risiko gagal.

Selanjutnya, kita menyusun sebuah hashtable untuk mewakili kemajuan proses yang sedang dalam perulangan, dengan menggunakan variabel $process bersamaan dengan merujuk pada kunci-kunci hashtable yang disinkronkan. Aktivitas dan kunci Status digunakan sebagai nilai parameter untuk Write-Progress menampilkan status proses tiruan tertentu di bagian berikutnya.

Perulangan foreach hanyalah cara untuk mensimulasikan proses yang berjalan dan diacak berdasarkan atribut $dataSetTunggu untuk menyetel Start-Sleep menggunakan milidetik. Bagaimana Anda menghitung kemajuan proses Anda dapat bervariasi.

Menampilkan kemajuan beberapa proses

Sekarang setelah proses tiruan berjalan sebagai pekerjaan, kita dapat mulai menulis kemajuan proses ke jendela 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
}

Variabel $job berisi pekerjaan induk dan memiliki pekerjaan anak untuk setiap proses tiruan. Selama salah satu pekerjaan anak masih berjalan, pekerjaan induk State akan tetap "Berjalan". Ini memungkinkan kita untuk menggunakan perulangan while untuk terus memperbarui kemajuan setiap proses sampai semua proses selesai.

Dalam perulangan while, kita mengiterasi setiap kunci di variabel $sync. Karena ini adalah hashtable yang disinkronkan, hashtable selalu diperbarui tetapi masih dapat diakses tanpa menghasilkan kesalahan apa pun.

Ada pemeriksaan untuk memastikan bahwa proses yang dilaporkan benar-benar berjalan menggunakan metode .IsNullOrEmpty() Jika proses belum dimulai, perulangan tidak akan melaporkannya dan akan melanjutkan ke proses berikutnya sampai menemukan proses yang telah dimulai. Jika proses dimulai, hash table dari kunci saat ini digunakan untuk menyebar parameter ke Write-Progress.

Contoh lengkap

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