다음을 통해 공유


Foreach Parallel을 사용하여 여러 스레드에서 작문 진행

PowerShell 7.0부터 Foreach-Object cmdlet의 병렬 매개 변수를 사용하여 동시에 여러 스레드에서 작업할 수 있습니다. 그렇지만 스레드의 진행률을 모니터링하는 것이 문제일 수 있습니다. 일반적으로 Write-Progress를 사용하여 프로세스의 진행률을 모니터링할 수 있습니다. 그러나 Parallel을 사용하면 PowerShell에서 각 스레드에 별도의 runspace를 사용하기 때문에 진행률을 다시 호스트에 보고하는 것이 일반적인 Write-Progress 사용만큼 간단하지 않습니다.

동기화된 해시 테이블을 사용하여 진행률 추적

여러 스레드에서 진행률을 작성할 때는 PowerShell에서 병렬 프로세스를 실행할 때 각 프로세스에 고유한 Runspace가 있기 때문에 추적이 어려워집니다. 이를 해결하려면 동기화된 해시 테이블을 사용할 수 있습니다. 동기화된 해시 테이블은 오류를 throw하지 않고 동시에 여러 스레드에서 수정할 수 있는 스레드로부터 안전한 데이터 구조입니다.

설정

이 방법의 단점 중 하나는 모든 것이 오류 없이 실행되도록 다소 복잡한 설정이 필요하기 때문에 발생합니다.

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

이 섹션에서는 세 가지 용도로 세 가지 데이터 구조를 만듭니다.

변수는 $dataSet 수정될 위험 없이 다음 단계를 조정하는 데 사용되는 해시 테이블 배열을 저장합니다. 컬렉션을 반복하는 동안 개체 컬렉션이 수정되면 PowerShell에서 오류가 발생합니다. 루프의 개체 컬렉션을 수정할 개체와 별도로 유지해야 합니다. 각 해시 테이블의 Id 키는 모의 프로세스의 식별자입니다. 키는 Wait 추적 중인 각 모의 프로세스의 워크로드를 시뮬레이션합니다.

변수는 $origin 각 키가 모의 프로세스 ID 중 하나인 중첩된 해시 테이블을 저장합니다. 그런 다음, 변수에 저장된 동기화된 해시 테이블을 하이드레이션하는 $sync 데 사용됩니다. $sync 변수는 진행률을 표시하는 부모 runspace에 진행률을 다시 보고하는 작업을 수행합니다.

프로세스 실행

이 섹션에서는 다중 스레드 프로세스를 실행하고 진행률을 표시하는 데 사용되는 일부 출력을 만듭니다.

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

모의 프로세스가 작업으로 Foreach-Object 전송되고 시작됩니다. ThrottleLimit는 큐에서 실행되는 여러 프로세스를 강조 표시하기 위해 3으로 설정됩니다. 작업은 변수에 $job 저장되며 나중에 모든 프로세스가 완료된 시기를 알 수 있습니다.

문을 사용하여 using: PowerShell에서 부모 범위 변수를 참조하는 경우 식을 사용하여 동적으로 만들 수 없습니다. 예를 들어 다음과 $process = $using:sync.$($PSItem.id)같이 변수를 $process 만들려고 하면 식을 사용할 수 없다는 오류가 발생합니다. 따라서 변수가 $syncCopy 실패할 위험 없이 변수를 참조하고 수정 $sync 할 수 있도록 변수를 만듭니다.

다음으로, 동기화된 해시 테이블 키를 참조하여 변수를 사용하여 $process 현재 루프에 있는 프로세스의 진행률을 나타내는 해시 테이블을 빌드합니다. 작업상태 키는 다음 섹션에서 지정된 모의 프로세스의 상태를 표시하기 위한 Write-Progress 매개 변수 값으로 사용됩니다.

foreach 루프는 프로세스 작동을 시뮬레이트하는 한 가지 방법일 뿐이며 밀리초를 사용하여 Start-Sleep을 설정하는 $dataSet Wait 특성에 따라 무작위로 지정됩니다. 프로세스의 진행률을 계산하는 방법은 다를 수 있습니다.

여러 프로세스의 진행률 표시

모의 프로세스가 작업으로 실행되었으므로 이제 프로세스 진행률을 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
}

$job 변수는 부모 작업을 포함하며 각 모의 프로세스의 자식 작업을 포함합니다. 자식 작업이 계속 실행되는 동안 부모 작업 상태는 "실행 중"으로 유지됩니다. 이렇게 하면 루프를 while 사용하여 모든 프로세스가 완료될 때까지 모든 프로세스의 진행률을 지속적으로 업데이트할 수 있습니다.

while 루프 내에서 변수의 각 키를 반복합니다 $sync . 이는 동기화된 해시 테이블이므로 지속적으로 업데이트되지만 오류를 throw하지 않고 계속 액세스할 수 있습니다.

보고되는 프로세스가 실제로 메서드를 사용하여 실행되고 있는지 확인하는 검사가 있습니다 IsNullOrEmpty() . 프로세스가 시작되지 않은 경우 루프는 해당 프로세스를 보고하고 시작된 프로세스로 이동할 때까지 다음으로 이동하지 않습니다. 프로세스가 시작되면 현재 키의 해시 테이블을 사용하여 매개 변수를 다음으로 스플래트합니다 Write-Progress.

전체 예제

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