如何在不使用作业的情况下并行运行我的PowerShell脚本?

如果我有一个需要在多台计算机上运行的脚本,或者有多个不同的参数,那么我怎样才能并行执行它,而不必担心会在Start-Job中产生一个新的PSJob ?

例如, 我想重新同步所有域成员上的时间 ,如下所示:

 $computers = Get-ADComputer -filter * |Select-Object -ExpandProperty dnsHostName $creds = Get-Credential domain\user foreach($computer in $computers) { $session = New-PSSession -ComputerName $computer -Credential $creds Invoke-Command -Session $session -ScriptBlock { w32tm /resync /nowait /rediscover } } 

但我不想等待每个PSSession连接并调用该命令。 没有乔布斯,这怎么可以同时完成呢?

更新 – 虽然这个答案解释了PowerShell运行空间的过程和机制,以及它们如何能够帮助你multithreading的非连续工作负载,但是同事PowerShell爱好者Warren的“Cookie Monster”F已经走了更远的路,并将这些相同的概念合并到一个工具中所谓的 Invoke-Parallel – 就是我在下面描述的,而且他已经使用可选的开关进行了扩展,用于logging和准备会话状态,包括导入的模块,非常酷的东西 – 我强烈build议您在构build自己的shiny解决scheme之前检查一下 。


使用并行运行空间执行:

减less不可避免的等待时间

在原来的特定情况下,调用的可执行文件有一个/nowait选项,可以防止在作业(在这种情况下,时间重新同步)完成时阻塞调用线程。

这从发行人的angular度大大减less了总体执行时间,但连接到每台机器仍然按顺序进行。 由于超时等待的积累,依次连接到数千个客户端可能需要很长时间,这取决于出于某种原因或其他原因而不可访问的计算机的数量。

为了避免在单个或多个连续超时的情况下排队所有后续连接,我们可以分派连接和调用命令的作业来分离并行执行的PowerShell运行空间。

什么是运行空间?

运行空间是您的PowerShell代码执行的虚拟容器,并从PowerShell语句/命令的angular度来表示/保存环境。

概括而言,1个运行空间= 1个执行线程,所以我们需要“multithreading”我们的PowerShell脚本是一个运行空间的集合,然后可以并行执行。

像原来的问题一样,调用命令多个运行空间的工作可以分解为:

  1. 创build一个RunspacePool
  2. 将一个PowerShell脚本或一个等效的可执行代码分配给RunspacePool
  3. 以asynchronous方式调用代码(即不必等待代码返回)

运行空间池模板

PowerShell有一个名为[RunspaceFactory]的types加速器,它将帮助我们创build运行空间组件 – 让我们来运行它

1.创build一个RunspacePool并Open()它:

 $RunspacePool = [runspacefactory]::CreateRunspacePool(1,8) $RunspacePool.Open() 

传递给CreateRunspacePool()18的两个参数是允许在任何给定时间执行的最小和最大数量的运行空间,给予我们有效的最大并行度8。

2.创build一个PowerShell的实例,附加一些可执行代码并将其分配给我们的RunspacePool:

PowerShell的实例与powershell.exe进程(实际上是一个主机应用程序)不同,但它是一个内部运行时对象,表示要执行的PowerShell代码。 我们可以使用[powershell]types加速器在PowerShell中创build一个新的PowerShell实例:

 $Code = { param($Credentials,$ComputerName) $session = New-PSSession -ComputerName $ComputerName -Credential $Credentials Invoke-Command -Session $session -ScriptBlock {w32tm /resync /nowait /rediscover} } $PSinstance = [powershell]::Create().AddScript($Code).AddArgument($creds).AddArgument("computer1.domain.tld") $PSinstance.RunspacePool = $RunspacePool 

3.使用APMasynchronous调用PowerShell实例:

使用.NET开发术语中的已知术语作为asynchronous编程模型 ,我们可以将命令的调用分解为Begin方法,给出一个“绿灯”来执行代码,以及一个End方法来收集结果。 由于我们在这种情况下并不真正对任何反馈感兴趣(我们不等待w32tm的输出),我们可以简单地通过调用第一个方法

 $PSinstance.BeginInvoke() 

将其包装在RunspacePool中

使用上述技术,我们可以包装创build新连接的顺序迭代,并在并行执行stream程中调用远程命令:

 $ComputerNames = Get-ADComputer -filter * -Properties dnsHostName |select -Expand dnsHostName $Code = { param($Credentials,$ComputerName) $session = New-PSSession -ComputerName $ComputerName -Credential $Credentials Invoke-Command -Session $session -ScriptBlock {w32tm /resync /nowait /rediscover} } $creds = Get-Credential domain\user $rsPool = [runspacefactory]::CreateRunspacePool(1,8) $rsPool.Open() foreach($ComputerName in $ComputerNames) { $PSinstance = [powershell]::Create().AddScript($Code).AddArgument($creds).AddArgument($ComputerName) $PSinstance.RunspacePool = $rsPool $PSinstance.BeginInvoke() } 

假设CPU有能力一次执行所有8个运行空间,我们应该能够看到执行时间大大减less,但是由于使用了相当“高级”的方法,脚本的可读性成本很高。


确定最佳的平行度:

我们可以很容易地创build一个RunspacePool,它允许同时执行100个运行空间:

 [runspacefactory]::CreateRunspacePool(1,100) 

但在一天结束时,这一切都归结为我们的本地CPU可以处理多less个单元的执行。 换句话说,只要你的代码正在执行,那么允许你有更多的逻辑处理器来运行代码的运行空间是没有任何意义的。

感谢WMI,这个门槛很容易确定:

 $NumberOfLogicalProcessor = (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors [runspacefactory]::CreateRunspacePool(1,$NumberOfLogicalProcessors) 

另一方面,如果你正在执行的代码由于networking延迟等外部因素而需要等待很长时间,那么你仍然可以从运行更多同时具有逻辑处理器的同步运行空间中受益,所以你可能要testing范围可能的最大运行空间find盈亏平衡

 foreach($n in ($NumberOfLogicalProcessors..($NumberOfLogicalProcessors*3))) { Write-Host "$n: " -NoNewLine (Measure-Command { $Computers = Get-ADComputer -filter * -Properties dnsHostName |select -Expand dnsHostName -First 100 ... [runspacefactory]::CreateRunspacePool(1,$n) ... }).TotalSeconds } 

除此之外,还缺less一个收集器,用于存储从运行空间创build的数据,以及一个用于检查运行空间状态的variables,即它是否已完成。

 #Add an collector object that will store the data $Object = New-Object 'System.Management.Automation.PSDataCollection[psobject]' #Create a variable to check the status $Handle = $PSinstance.BeginInvoke($Object,$Object) #So if you want to check the status simply type: $Handle #If you want to see the data collected, type: $Object