Bagikan melalui


Mengoptimalkan performa menggunakan eksekusi paralel

PowerShell menyediakan beberapa opsi untuk pembuatan pemanggilan paralel.

  • Start-Job menjalankan setiap tugas dalam proses terpisah, masing-masing dengan instans baru PowerShell. Dalam banyak kasus, perulangan linier lebih cepat. Selain itu, serialisasi dan deserialisasi dapat membatasi kegunaan objek yang dikembalikan. Perintah ini tertanam dalam semua versi PowerShell.
  • Start-ThreadJob adalah cmdlet yang ditemukan dalam modul ThreadJob . Perintah ini menggunakan ruang kerja PowerShell untuk mengelola dan membuat tugas berbasis utas. Pekerjaan ini lebih ringan daripada pekerjaan yang dibuat oleh Start-Job dan menghindari potensi hilangnya keakuratan jenis yang diperlukan oleh serialisasi lintas proses dan deserialisasi. Modul ThreadJob dilengkapi dengan PowerShell 7 dan yang lebih tinggi. Untuk Windows PowerShell 5.1, Anda dapat menginstal modul ini dari Galeri PowerShell.
  • Gunakan namespace System.Management.Automation.Runspaces dari PowerShell SDK untuk membuat logika paralel Anda sendiri. Baik ForEach-Object -Parallel dan Start-ThreadJob gunakan runspace PowerShell untuk menjalankan kode secara paralel.
  • Alur kerja adalah fitur Windows PowerShell 5.1. Alur kerja tidak tersedia di PowerShell 7.0 dan yang lebih tinggi. Alur kerja adalah jenis skrip PowerShell khusus yang dapat berjalan secara paralel. Mereka dirancang untuk tugas yang berjalan lama dan dapat dijeda dan dilanjutkan. Alur kerja tidak disarankan untuk pengembangan baru. Untuk informasi selengkapnya, lihat about_Workflows.
  • ForEach-Object -Parallel adalah fitur PowerShell 7.0 dan yang lebih tinggi. Seperti Start-ThreadJob, menggunakan runspace PowerShell untuk membuat dan mengelola pekerjaan berbasis thread. Perintah ini dirancang untuk digunakan dalam jalur proses.

Membatasi kesamaan pemrosesan eksekusi

Menjalankan skrip secara paralel tidak menjamin peningkatan performa. Misalnya, skenario berikut dapat memperoleh manfaat dari eksekusi paralel:

  • Pemrosesan skrip yang membutuhkan pemrosesan tinggi pada prosesor multi-inti multi-utas
  • Skrip yang menghabiskan waktu menunggu hasil atau melakukan operasi file, selama operasi tersebut tidak saling memblokir.

Penting untuk menyeimbangkan biaya eksekusi paralel dengan jenis pekerjaan yang dilakukan. Selain itu, ada batasan jumlah pemanggilan yang dapat berjalan secara paralel.

Perintah Start-ThreadJob dan ForEach-Object -Parallel memiliki parameter ThrottleLimit untuk membatasi jumlah pekerjaan yang berjalan sekaligus. Saat lebih banyak pekerjaan dimulai, pekerjaan diantrekan dan menunggu hingga jumlah pekerjaan saat ini turun di bawah batas pembatasan. Mulai PowerShell 7.1, ForEach-Object -Parallel secara otomatis menggunakan ulang ruang kerja dari kumpulan ruang kerja. Parameter ThrottleLimit mengatur ukuran kumpulan runspace. Ukuran kumpulan runspace default adalah 5. Anda masih dapat membuat runspace baru untuk setiap iterasi menggunakan sakelar UseNewRunspace.

Perintah Start-Job tidak memiliki parameter ThrottleLimit . Anda harus mengelola jumlah pekerjaan yang berjalan pada satu waktu.

Mengukur performa

Fungsi berikut, Measure-Parallel, membandingkan kecepatan pendekatan eksekusi paralel berikut:

  • Start-Job - membuat proses PowerShell anak di belakang layar

  • Start-ThreadJob - menjalankan setiap tugas dalam utas terpisah

  • ForEach-Object -Parallel - menjalankan setiap tugas dalam utas terpisah

  • Start-Process - memanggil program eksternal secara asinkron

    Nota

    Pendekatan ini hanya masuk akal jika tugas paralel Anda hanya terdiri dari satu panggilan ke program eksternal, dibandingkan dengan menjalankan blok kode PowerShell. Selain itu, satu-satunya cara untuk menangkap output dengan pendekatan ini adalah dengan mengalihkan ke file.

function Measure-Parallel {
    [CmdletBinding()]
    param(
        [ValidateRange(2, 2147483647)]
        [int] $BatchSize = 5,

        [ValidateSet('Job', 'ThreadJob', 'Process', 'ForEachParallel', 'All')]
        [string[]] $Approach,

        # pass a higher count to run multiple batches
        [ValidateRange(2, 2147483647)]
        [int] $JobCount = $BatchSize
    )

    $noForEachParallel = $PSVersionTable.PSVersion.Major -lt 7
    $noStartThreadJob = -not (Get-Command -ErrorAction Ignore Start-ThreadJob)

    # Translate the approach arguments into their corresponding hashtable keys (see below).
    if ('All' -eq $Approach) { $Approach = 'Job', 'ThreadJob', 'Process', 'ForEachParallel' }
    $approaches = $Approach.ForEach({
            if ($_ -eq 'ForEachParallel') { 'ForEach-Object -Parallel' }
            else { $_ -replace '^', 'Start-' }
        })

    if ($noStartThreadJob) {
        if ($interactive -or $approaches -contains 'Start-ThreadJob') {
            Write-Warning "Start-ThreadJob is not installed, omitting its test."
            $approaches = $approaches.Where({ $_ -ne 'Start-ThreadJob' })
        }
    }
    if ($noForEachParallel) {
        if ($interactive -or $approaches -contains 'ForEach-Object -Parallel') {
            Write-Warning 'ForEach-Object -Parallel require PowerShell v7+, omitting its test.'
            $approaches = $approaches.Where({ $_ -ne 'ForEach-Object -Parallel' })
        }
    }

    # Simulated input: Create 'f0.zip', 'f1'.zip', ... file names.
    $zipFiles = 0..($JobCount - 1) -replace '^', 'f' -replace '$', '.zip'

    # Sample executables to run - here, the native shell is called to simply
    # echo the argument given.
    $exe = if ($env:OS -eq 'Windows_NT') { 'cmd.exe' } else { 'sh' }

    # The list of its arguments *as a single string* - use '{0}' as the placeholder
    # for where the input object should go.
    $exeArgList = if ($env:OS -eq 'Windows_NT') {
        '/c "echo {0} > NUL:"'
     } else {
        '-c "echo {0} > /dev/null"'
    }

    # A hashtable with script blocks that implement the 3 approaches to parallelism.
    $approachImpl = [ordered] @{}

    # child-process-based job
    $approachImpl['Start-Job'] = {
        param([array] $batch)
        $batch |
            ForEach-Object {
                Start-Job {
                    Invoke-Expression ($using:exe + ' ' + ($using:exeArgList -f $args[0]))
                } -ArgumentList $_
            } |
            Receive-Job -Wait -AutoRemoveJob | Out-Null
    }

    # thread-based job - requires the ThreadJob module
    if (-not $noStartThreadJob) {
        # If Start-ThreadJob is available, add an approach for it.
        $approachImpl['Start-ThreadJob'] = {
            param([array] $batch)
            $batch |
                ForEach-Object {
                    Start-ThreadJob -ThrottleLimit $BatchSize {
                        Invoke-Expression ($using:exe + ' ' + ($using:exeArgList -f $args[0]))
                    } -ArgumentList $_
                } |
                Receive-Job -Wait -AutoRemoveJob | Out-Null
        }
    }

    # ForEach-Object -Parallel job
    if (-not $noForEachParallel) {
        $approachImpl['ForEach-Object -Parallel'] = {
            param([array] $batch)
            $batch | ForEach-Object -ThrottleLimit $BatchSize -Parallel {
                Invoke-Expression ($using:exe + ' ' + ($using:exeArgList -f $_))
            }
        }
    }

    # direct execution of an external program
    $approachImpl['Start-Process'] = {
        param([array] $batch)
        $batch |
            ForEach-Object {
                Start-Process -NoNewWindow -PassThru $exe -ArgumentList ($exeArgList -f $_)
            } |
            Wait-Process
    }

    # Partition the array of all indices into subarrays (batches)
    $batches = @(
        0..([math]::Ceiling($zipFiles.Count / $batchSize) - 1) | ForEach-Object {
            , $zipFiles[($_ * $batchSize)..($_ * $batchSize + $batchSize - 1)]
        }
    )

    $tsTotals = foreach ($appr in $approaches) {
        $i = 0
        $tsTotal = [timespan] 0
        $batches | ForEach-Object {
            Write-Verbose "$batchSize-element '$appr' batch"
            $ts = Measure-Command { & $approachImpl[$appr] $_ | Out-Null }
            $tsTotal += $ts
            if (++$i -eq $batches.Count) {
                # last batch processed.
                if ($batches.Count -gt 1) {
                    Write-Verbose ("'$appr' processing $JobCount items finished in " +
                        "$($tsTotal.TotalSeconds.ToString('N2')) secs.")
                }
                $tsTotal # output the overall timing for this approach
            }
        }
    }

    # Output a result object with the overall timings.
    $oht = [ordered] @{}
    $oht['JobCount'] = $JobCount
    $oht['BatchSize'] = $BatchSize
    $oht['BatchCount'] = $batches.Count
    $i = 0
    foreach ($appr in $approaches) {
        $oht[($appr + ' (secs.)')] = $tsTotals[$i++].TotalSeconds.ToString('N2')
    }
    [pscustomobject] $oht
}

Contoh berikut menggunakan Measure-Parallel untuk menjalankan 20 pekerjaan secara paralel, 5 pada satu waktu, menggunakan semua pendekatan yang tersedia.

Measure-Parallel -Approach All -BatchSize 5 -JobCount 20 -Verbose

Output berikut berasal dari komputer Windows yang menjalankan PowerShell 7.5.1. Waktu Anda dapat bervariasi berdasarkan banyak faktor, tetapi rasionya harus memberikan gambaran kinerja relatif.

VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 'Start-Job' processing 20 items finished in 7.58 secs.
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 'Start-ThreadJob' processing 20 items finished in 2.37 secs.
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 'Start-Process' processing 20 items finished in 0.26 secs.
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 'ForEach-Object -Parallel' processing 20 items finished in 0.79 secs.

JobCount                         : 20
BatchSize                        : 5
BatchCount                       : 4
Start-Job (secs.)                : 7.58
Start-ThreadJob (secs.)          : 2.37
Start-Process (secs.)            : 0.26
ForEach-Object -Parallel (secs.) : 0.79

Kesimpulan

  • Pendekatan ini Start-Process berkinerja terbaik karena tidak memiliki overhead manajemen pekerjaan. Namun, seperti yang disebutkan sebelumnya, pendekatan ini memiliki batasan mendasar.
  • ForEach-Object -Parallel menambahkan overhead paling sedikit, diikuti oleh Start-ThreadJob.
  • Start-Job memiliki beban kerja paling banyak karena instans PowerShell tersembunyi yang dibuatnya untuk setiap tugas.

Pengakuan

Sebagian besar informasi adalah artikel ini didasarkan pada jawaban dari Santiago Squarzon dan mklement0 dalam posting Stack Overflow ini.

Anda mungkin juga tertarik dengan modul PSParallelPipeline yang dibuat oleh Santiago Squarzon.

Bacaan lebih lanjut