根据您问题中的代码,我无法解释您的症状(请参阅底部部分),但我建议您将解决方案基于(现在)标准 Start-ThreadJob cmdlet(随附 PowerShell Core;在 Windows PowerShell 中,使用Install-Module ThreadJob -Scope CurrentUser 安装它,例如[1]):
这样的解决方案比使用第三方 Invoke-Async 函数更有效,在撰写本文时,该函数存在缺陷,因为它在 紧密循环 中等待作业完成,这会创建不必要的处理开销。
Start-ThreadJob 作业是基于进程的Start-Job 后台作业的轻量级、基于线程的替代方案,但它们与标准作业管理cmdlet 集成,例如Wait-Job 和Receive-Job。
这是一个基于您的代码的独立示例,用于演示其用法:
注意:无论您使用Start-ThreadJob 还是Invoke-Async,您都无法在单独运行的脚本块中显式引用 [fileToCopy] 等自定义类线程(运行空间;见底部),因此为了简单起见,下面的解决方案仅使用具有感兴趣属性的[pscustomobject] 实例。
# Create sample CSV file with 10 rows.
$FileList = Join-Path ([IO.Path]::GetTempPath()) "tmp.$PID.csv"
@'
Foo,SrcFileName,DestFileName,Bar
1,c:\tmp\a,\\server\share\a,baz
2,c:\tmp\b,\\server\share\b,baz
3,c:\tmp\c,\\server\share\c,baz
4,c:\tmp\d,\\server\share\d,baz
5,c:\tmp\e,\\server\share\e,baz
6,c:\tmp\f,\\server\share\f,baz
7,c:\tmp\g,\\server\share\g,baz
8,c:\tmp\h,\\server\share\h,baz
9,c:\tmp\i,\\server\share\i,baz
10,c:\tmp\j,\\server\share\j,baz
'@ | Set-Content $FileList
# How many threads at most to run concurrently.
$NumCopyThreads = 8
Write-Host 'Creating jobs...'
$dtStart = [datetime]::UtcNow
# Import the CSV data and transform it to [pscustomobject] instances
# with only .SrcFileName and .DestFileName properties - they take
# the place of your original [fileToCopy] instances.
$jobs = Import-Csv $FileList | Select-Object SrcFileName, DestFileName |
ForEach-Object {
# Start the thread job for the file pair at hand.
Start-ThreadJob -ThrottleLimit $NumCopyThreads -ArgumentList $_ {
param($f)
$simulatedRuntimeMs = 2000 # How long each job (thread) should run for.
# Delay output for a random period.
$randomSleepPeriodMs = Get-Random -Minimum 100 -Maximum $simulatedRuntimeMs
Start-Sleep -Milliseconds $randomSleepPeriodMs
# Produce output.
"Copied $($f.SrcFileName) to $($f.DestFileName)"
# Wait for the remainder of the simulated runtime.
Start-Sleep -Milliseconds ($simulatedRuntimeMs - $randomSleepPeriodMs)
}
}
Write-Host "Waiting for $($jobs.Count) jobs to complete..."
# Synchronously wait for all jobs (threads) to finish and output their results
# *as they become available*, then remove the jobs.
# NOTE: Output will typically NOT be in input order.
Receive-Job -Job $jobs -Wait -AutoRemoveJob
Write-Host "Total time lapsed: $([datetime]::UtcNow - $dtStart)"
# Clean up the temp. file
Remove-Item $FileList
上面的结果类似于:
Creating jobs...
Waiting for 10 jobs to complete...
Copied c:\tmp\b to \\server\share\b
Copied c:\tmp\g to \\server\share\g
Copied c:\tmp\d to \\server\share\d
Copied c:\tmp\f to \\server\share\f
Copied c:\tmp\e to \\server\share\e
Copied c:\tmp\h to \\server\share\h
Copied c:\tmp\c to \\server\share\c
Copied c:\tmp\a to \\server\share\a
Copied c:\tmp\j to \\server\share\j
Copied c:\tmp\i to \\server\share\i
Total time lapsed: 00:00:05.1961541
请注意,接收到的输出不反映输入顺序,并且总体运行时间大约是每线程运行时间 2 秒(加上开销)的 2 倍,因为 2 个“批次”有由于输入计数为 10 而运行,而只有 8 个线程可用。
如果您将线程数增加到 10 或更多(默认为 50),整体运行时间将下降到 2 秒加上开销,因为所有作业随后都会同时运行。
警告:以上数字源于在 Microsoft Windows 10 Pro(64 位;版本 1903)上运行的 PowerShell Core 版本,使用版本 2.0.1 ThreadJob 模块。
令人费解的是,同样的代码在 Windows PowerShell v5.1.18362.145 中慢得多。
但是,对于性能和内存消耗,最好在您的情况下使用批处理(分块),即每个线程处理多个文件对。
以下解决方案演示了这种方法;调整 $chunkSize 以找到适合您的批量大小。
# Create sample CSV file with 10 rows.
$FileList = Join-Path ([IO.Path]::GetTempPath()) "tmp.$PID.csv"
@'
Foo,SrcFileName,DestFileName,Bar
1,c:\tmp\a,\\server\share\a,baz
2,c:\tmp\b,\\server\share\b,baz
3,c:\tmp\c,\\server\share\c,baz
4,c:\tmp\d,\\server\share\d,baz
5,c:\tmp\e,\\server\share\e,baz
6,c:\tmp\f,\\server\share\f,baz
7,c:\tmp\g,\\server\share\g,baz
8,c:\tmp\h,\\server\share\h,baz
9,c:\tmp\i,\\server\share\i,baz
10,c:\tmp\j,\\server\share\j,baz
'@ | Set-Content $FileList
# How many threads at most to run concurrently.
$NumCopyThreads = 8
# How many files to process per thread
$chunkSize = 3
# The script block to run in each thread, which now receives a
# $chunkSize-sized *array* of file pairs.
$jobScriptBlock = {
param([pscustomobject[]] $filePairs)
$simulatedRuntimeMs = 2000 # How long each job (thread) should run for.
# Delay output for a random period.
$randomSleepPeriodMs = Get-Random -Minimum 100 -Maximum $simulatedRuntimeMs
Start-Sleep -Milliseconds $randomSleepPeriodMs
# Produce output for each pair.
foreach ($filePair in $filePairs) {
"Copied $($filePair.SrcFileName) to $($filePair.DestFileName)"
}
# Wait for the remainder of the simulated runtime.
Start-Sleep -Milliseconds ($simulatedRuntimeMs - $randomSleepPeriodMs)
}
Write-Host 'Creating jobs...'
$dtStart = [datetime]::UtcNow
$jobs = & {
# Process the input objects in chunks.
$i = 0
$chunk = [pscustomobject[]]::new($chunkSize)
Import-Csv $FileList | Select-Object SrcFileName, DestFileName | ForEach-Object {
$chunk[$i % $chunkSize] = $_
if (++$i % $chunkSize -ne 0) { return }
# Note the need to wrap $chunk in a single-element helper array (, $chunk)
# to ensure that it is passed *as a whole* to the script block.
Start-ThreadJob -ThrottleLimit $NumCopyThreads -ArgumentList (, $chunk) -ScriptBlock $jobScriptBlock
$chunk = [pscustomobject[]]::new($chunkSize) # we must create a new array
}
# Process any remaining objects.
# Note: $chunk -ne $null returns those elements in $chunk, if any, that are non-null
if ($remainingChunk = $chunk -ne $null) {
Start-ThreadJob -ThrottleLimit $NumCopyThreads -ArgumentList (, $remainingChunk) -ScriptBlock $jobScriptBlock
}
}
Write-Host "Waiting for $($jobs.Count) jobs to complete..."
# Synchronously wait for all jobs (threads) to finish and output their results
# *as they become available*, then remove the jobs.
# NOTE: Output will typically NOT be in input order.
Receive-Job -Job $jobs -Wait -AutoRemoveJob
Write-Host "Total time lapsed: $([datetime]::UtcNow - $dtStart)"
# Clean up the temp. file
Remove-Item $FileList
虽然输出实际上是相同的,但请注意这次仅创建 4 个作业,每个作业处理(最多)$chunkSize (3) 文件对。
至于你尝试了什么:
您显示的屏幕截图表明问题在于您的自定义类[fileToCopy] 对Invoke-Async 运行的脚本块不可见。
由于Invoke-Async 在对调用者状态一无所知的单独运行空间中通过 PowerShell SDK 调用脚本块,因此可以预期这些运行空间不知道您的类(这同样适用于 Start-ThreadJob)。
但是,不清楚为什么这是您的代码中的问题,因为 您的脚本块没有明确引用您的类:您的脚本块参数 $file 不是类型- 约束(隐含[object]-typed)。
因此,只需在脚本块中访问自定义类实例的 properties应该 工作,并且在我对 Windows PowerShell v5.1.18362.145 的测试中确实如此在 Microsoft Windows 10 Pro(64 位;版本 1903)上。
但是,如果您的真实脚本块代码明确引用自定义类 [fileToCopy] - 例如通过将参数定义为 param([fileToToCopy] $file) - 您会看到症状强>.
[1] 在不附带 PowerShellGet 模块的 Windows PowerShell v3 和 v4 中,Install-Module 默认不可用。但是,该模块可以按需安装,如Installing PowerShellGet 中所述。