【问题标题】:Copy-item using invoke-async in Powershell在 Powershell 中使用调用异步复制项目
【发布时间】:2019-12-31 16:28:48
【问题描述】:

本文介绍如何在 PowerShell 中使用 Invoke-Async:https://sqljana.wordpress.com/2018/03/16/powershell-sql-server-run-in-parallel-collect-sql-results-with-print-output-from-across-your-sql-farm-fast/

我希望在 PowerShell 中并行运行 copy-item cmdlet,因为另一种方法是通过 Excel 使用 FileSystemObject 并从数百万个文件中一次复制一个文件。

我拼凑了以下内容:

.SYNOPSIS
<Brief description>
For examples type:
Get-Help .\<filename>.ps1 -examples
.DESCRIPTION
Copys files from one path to another
.PARAMETER FileList
e.g. C:\path\to\list\of\files\to\copy.txt
.PARAMETER NumCopyThreads
default is 8 (but can be 100 if you want to stress the machine to maximum!)
.EXAMPLE
.\CopyFilesToBackup -filelist C:\path\to\list\of\files\to\copy.txt
.NOTES
#>

[CmdletBinding()] 
Param( 
    [String] $FileList = "C:\temp\copytest.csv", 
    [int] $NumCopyThreads = 8
) 

$filesToCopy = New-Object "System.Collections.Generic.List[fileToCopy]"
$csv = Import-Csv $FileList

foreach($item in $csv)
{
    $file = New-Object fileToCopy
    $file.SrcFileName = $item.SrcFileName
    $file.DestFileName = $item.DestFileName
    $filesToCopy.add($file)
}

$sb = [scriptblock] {
    param($file)
    Copy-item -Path $file.SrcFileName -Destination $file.DestFileName
}
$results = Invoke-Async -Set $filesToCopy -SetParam file -ScriptBlock $sb -Verbose -Measure:$true -ThreadCount 8
$results | Format-Table

Class fileToCopy {
    [String]$SrcFileName = ""
    [String]$DestFileName = ""
}

csv 输入如下所示:

SrcFileName,DestFileName
C:\Temp\dummy-data\101438\101438-0154723869.zip,\\backupserver\Project Archives\101438\0154723869.zip
C:\Temp\dummy-data\101438\101438-0165498273.xlsx,\\backupserver\Project Archives\101438\0165498273.xlsx

我缺少什么才能使它正常工作,因为当我运行 .\CopyFiles.ps1 -FileList C:\Temp\test.csv 时没有任何反应。文件存在于源路径中,但未从 -Set 集合中提取文件对象。 (除非我误解了集合是如何使用的?)

不,我不能使用 robocopy 来执行此操作,因为有数百万个文件会根据其原始位置解析为不同的路径。

【问题讨论】:

  • 将“fileToCopy”的类声明放在你使用它之前的行 $filesToCopy = New-Object "System.Collections.Generic.List[fileToCopy]"
  • @f6a4:与函数不同,类在执行前被解析,因此被“提升”。也就是说,它们不需要在之前被定义;试试[foo].Name; class Foo {}
  • 谢谢 mklement0 我知道我所拥有的是正确的;直到 Invoke-Async 行的代码都可以正常工作。如果@f6a4 所说的是真的,那它根本行不通。并且移动类声明不会影响 Invoke-Async 生成的整体“耸肩”
  • .\CopyFiles.ps1 -FileList C:\temp\test.csv -Verbose 输出什么有趣的东西吗?
  • 它现在所做的只是输出一堆错误。我发现的一件事是,当我成功地将我创建的类传递给 Invoke-Async 模块时,它抱怨它找不到类型。我试过使用 PSCustomObject 但无济于事。

标签: powershell copy-item


【解决方案1】:

根据您问题中的代码,我无法解释您的症状(请参阅底部部分),但我建议您将解决方案基于(现在)标准 Start-ThreadJob cmdlet(随附 PowerShell Core;在 Windows PowerShell 中,使用Install-Module ThreadJob -Scope CurrentUser 安装它,例如[1]):

这样的解决方案比使用第三方 Invoke-Async 函数更有效,在撰写本文时,该函数存在缺陷,因为它在 紧密循环 中等待作业完成,这会创建不必要的处理开销。

Start-ThreadJob 作业是基于进程的Start-Job 后台作业的轻量级、基于线程的替代方案,但它们与标准作业管理cmdlet 集成,例如Wait-JobReceive-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 中所述。

【讨论】:

  • 我直到现在才看到这个,这里已经很晚了@mklement0,所以我会在早上阅读并消化这个。非常感谢您的意见。
  • @AlexFielder:我的荣幸;这对我来说也是一次学习经历——我希望我做对了;如果没有,很乐意修改答案。
  • 我花了太长时间看这个(事实上也是今晚的大部分时间),虽然我一直在这里愉快地测试答案的变体:stackoverflow.com/a/41797153/572634我喜欢你的更多的是因为它的视觉简单/可读性。毫无疑问,明天早上我会有一些问题,但我会带着这个令人讨厌的功能到达办公室。你成就了我的一天/一周。 :-)
  • @AlexFielder:我认为是的,但请注意,如果输出与输入的顺序相同,则需要额外的工作。另一件需要注意的事情:为每个文件创建 1 个线程可能性能不佳;也许 batching 有帮助(每个线程多个文件)。
  • @AlexFielder:您必须实现自己的输入批处理(分块) - 请参阅我的更新;添加的解决方案允许您尝试批量大小。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-12
  • 2012-01-28
  • 1970-01-01
  • 1970-01-01
  • 2016-12-28
  • 1970-01-01
相关资源
最近更新 更多