【问题标题】:How can I speed up a PowerShelll foreach loop如何加快 PowerShell foreach 循环
【发布时间】:2020-01-31 16:43:15
【问题描述】:

我有一个 PowerShell 脚本,它连接到数据库并提取用户数据列表。我获取这些数据并创建一个foreach 循环来运行数据脚本。

这是可行的,但速度很慢,因为结果可能是 1000 多个条目,并且它必须完成 Script.batUser A 才能启动 User B。单个用户的Script.bat 独立于另一个用户,每个用户大约需要 30 秒。

有没有办法加快这个速度?我一直在玩-ParallelForEach-Objectworkflow,但我无法让它工作,可能是因为我是 PS 的菜鸟。

foreach ($row in $Dataset.tables[0].rows)
{
   $UserID=$row.value
   $DeviceID=$row.value1
   $EmailAddress=$row.email_address

   cmd.exe /c "`"$PSScriptRoot`"\bin\Script.bat -c `" -Switch $UserID`" >> `"$PSScriptRoot`"\${FileName3}_REST_${DateTime}.txt 2> nul";
}

【问题讨论】:

  • 所以你说你想加快速度,但唯一提到的瓶颈似乎是有问题的 .BAT 文件。 需要半分钟的 .BAT 做什么? 似乎这将是要调查的相关领域,而不是上述 sn-p。
  • forloop 是我需要纠正的问题。 .bat 文件预计需要 30 秒,问题是 forloop 需要一个接一个地调用 .bat 文件 1000 次。我正在尝试找到一种方法来调用 bat 文件并开始下一个循环,而无需 powershell 等待 .bat 的执行完成。试图让 forloop 多任务:)
  • 尝试使用作业:sconstantinou.com/powershell-jobs

标签: powershell cmd


【解决方案1】:

您自己说过,您的瓶颈在于脚本中的批处理文件,而不是循环本身。 foreach(相对于ForEach-Object)已经是PowerShell 中更快的foreach 循环机制。 调查您的批处理文件,找出为什么需要 30 秒才能完成,并尽可能优化它。


使用作业

注意:Start-Job 将在另一个进程下运行该作业。如果您有 PowerShell Core,则可以使用 Start-ThreadJob cmdlet 代替 Start-Job。这将作为同一进程的另一个线程的一部分启动您的工作,而不是启动另一个进程。

如果您无法优化您的批处理脚本或优化它以满足您的需求,那么您可以考虑使用Start-Job 启动作业以异步执行,然后使用@ 检查结果并从中获取任何输出987654324@。例如:

# Master list of jobs you need to check the result of later
$jobs = New-Object System.Collections.Generic.List[System.Management.Automation.Job]

# Run your script for each row
foreach ($row in $Dataset.tables[0].rows)
{
   $UserID=$row.value
   $DeviceID=$row.value1
   $EmailAddress=$row.email_address

   # Use Start-Job here to kick off the script and store the job information
   # for later retrieval.
   # The $using: scope modifier allows you to make use of variables that were
   # defined in the session calling Start-Job
   $job = Start-Job -ScriptBlock { cmd.exe /c "`"${using:PSScriptRoot}`"\bin\Script.bat -c `" -Switch ${using:UserID}`" >> `"${using:PSScriptRoot}`"\${using:FileName3}_REST_${DateTime}.txt 2> nul"; }

   # Add the execution to the $jobs list to check the result of later
   # Casting to void here prevents the Add method from returning the object
   # we've added.
   [void]$jobs.Add($job)
}

# Wait for the jobs to be done
Write-Host 'Waiting for all jobs to complete...'
while( $jobs | Where-Object { $_.State -eq 'Running' } ){
  Start-Sleep -s 10
}

# Retrieve the output of the jobs
foreach( $j in $jobs ) {
  Receive-Job $j
}

注意:由于您有大约 1000 次需要执行此脚本,您可能需要考虑编写逻辑以一次只运行一定数量的作业。我上面的示例启动了所有必要的作业,而不考虑可能一次执行的数量。


有关作业的更多信息以及您可以检查正在运行/已完成的作业的属性,请查看以下链接:

* 文档指出 using 范围只能在使用远程会话时声明,但 this seems to work fine with Start-Job 即使作业是本地的。

【讨论】:

  • 我刚试过这个,但似乎 ``` $job = Start-Job -ScriptBlock { }``` 之外的变量在 scriptBlock 中不可用。从您的示例中,当作业中的命令运行时,$UserID 为空。
  • 我更新了我的答案。您必须将参数传递给作业。我的示例参数在$jobArgs 对象上设置了UserID,但您也可以添加$DateTime$FileName3 的值。请注意,我的示例有效负载对这些值使用字符串,但它们可以是任何对象类型。我还修改了cmd 字符串以使用子表达式而不是字符串插值,因此我们可以从字符串中的$args 对象中获取属性。
  • 实际上,我做了另一个测试,看起来Start-Job 允许您使用$using: 修饰符来访问父会话中的变量,即使您不是在远程机器上执行。我已经更新了我的答案,在此处推荐 $using 修饰符。
  • 我去上班了。在我使用 $args[0]、$args[1] 等的 scriptBlock 中添加了$job = Start-Job -ScriptBlock { } -ArgumentList $UserID,$DeviceID,$EmailAddress,$Track1,$Track2,$PSScriptRoot,$FileName3,$DateTime
  • 是的,这就是为什么我更喜欢使用我最初添加的 hashmap 技巧,或者使用 using 范围修饰符。必须传入命名参数并在 位置 中引用它们在 IMO 中的可读性不是很高。
猜你喜欢
  • 2019-09-19
  • 2014-10-23
  • 2018-03-13
  • 2020-11-09
  • 1970-01-01
  • 2013-06-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多