PowerShell 技能连载 - 探索即插即用设备(第 2 部分)

在前一个技能中,我们使用 UPnP.UPnPDeviceFinder 来发现连入您网络的智能设备。今天,让我们仔细看看返回的对象。

1
2
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$result = $UPnPFinder.FindByType("upnp:rootdevice", 0)

当您运行一个搜索(可能需要执行一定的时间),将会获取到许多信息,但是某些属性只是以 System.__ComObject 的形式返回。如何查看它们背后隐藏的信息?

让我们取其中的一个返回对象。在我的例子中,通过查看 $result,我注意到一个来自 “NetGear” 对象。接下来,我使用 Where-Object 来存取它。

在您的场景中,可用的对象明显不同,这取决于您网络中使用的设备,所以您需要调整过滤器表达式来匹配返回的对象之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PS> $switch = $result | Where-Object ManufacturerName -like *NetGear*

PS> $switch


IsRootDevice : True
RootDevice : System.__ComObject
ParentDevice :
HasChildren : True
Children : System.__ComObject
UniqueDeviceName : uuid:4d696e69-444c-164e-9d42-3894ed0e1db5
FriendlyName : RBR50 (Gateway)
Type : urn:schemas-upnp-org:device:InternetGatewayDevice:1
PresentationURL : http://www.orbilogin.net/
ManufacturerName : NETGEAR, Inc.
ManufacturerURL : http://www.netgear.com/
ModelName : NETGEAR Orbi Desktop AC3000 Router
ModelNumber : RBR50
Description : http://www.netgear.com/home/products/wirelessrouters
ModelURL : http://www.netgear.com/orbi
UPC : RBR50
SerialNumber : 5R21945T06DA4
Services : System.__ComObject

大多数属性使用普通的数据类型,例如 string 或者 integer。例如,”UniqueDeviceName” 返回一个设备的唯一名称(稍后会变得很重要)。然而某些属性,只是返回 “System.__ComObject”。让我们看看这些:

每个 PnP 设备可以是一个链条的一部分,并且由一个根设备开始。由于我们一开始搜索的是根设备,所以 “RootDevice” 属性总是对应返回的对象:

1
2
PS> $switch -eq $switch.RootDevice
True

要找出那些设备链接到根设备,请查看 “HasChildren” 和 “Children”:”HasChildren” 是一个简单的 Boolean 值。然而 “Children” 是一个 “System.__ComObject” 对象并且包含了我们想了解的信息:

1
2
PS> if ($switch.HasChildren) { $switch.Children.Count } else { 'no children' }
1

显然地,”Children” 看起来是某种数组。总之,一个根设备可以有许多子设备。通过存取该属性,PowerShell 自动将 “System.__ComObject” 转换为可读取的 .NET 对象,以下是我的 NetGear 设备的子设备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PS> $switch.Children


IsRootDevice : False
RootDevice : System.__ComObject
ParentDevice : System.__ComObject
HasChildren : True
Children : System.__ComObject
UniqueDeviceName : uuid:4d696e69-444c-164e-9d43-3894ed0e1db5
FriendlyName : WAN Device
Type : urn:schemas-upnp-org:device:WANDevice:1
PresentationURL :
ManufacturerName : NETGEAR
ManufacturerURL : http://www.netgear.com/
ModelName : NETGEAR Orbi Desktop AC3000 Router
ModelNumber : RBR50
Description : WAN Device on NETGEAR RBR50 Orbi Router
ModelURL : http://www.netgear.com/
UPC : RBR50
SerialNumber : 5R21945T06DA4
Services : System.__ComObject

这个子设备之下还有子设备。但是,继续深挖并列出子设备之下的子设备看起来失败了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS> # works:
PS> $switch.Children | Select-Object HasChildren, Children

HasChildren Children
----------- --------
True System.__ComObject



PS> # fails:
PS> $switch.Children.HasChildren
PS> $switch.Children.Children
PS> $switch.Children[0].HasChildren
Value does not fall within the expected range.
PS> $switch.Children[0].Children
Value does not fall within the expected range.

失败的原因是 COM 数组的一个特异性。它们使用一个非常规的枚举器,和普通的对象数组不同,所以 PowerShell 无法直接存取数组元素。一个简单的解决方案是使用 ForEach-Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PS> $child = $switch.Children

PS> $child | ForEach-Object { $_.Children }


IsRootDevice : False
RootDevice : System.__ComObject
ParentDevice : System.__ComObject
HasChildren : False
Children : System.__ComObject
UniqueDeviceName : uuid:4d696e69-444c-164e-9d44-3894ed0e1db5
FriendlyName : WAN Connection Device
Type : urn:schemas-upnp-org:device:WANConnectionDevice:1
PresentationURL :
ManufacturerName : NETGEAR
ManufacturerURL : http://www.netgear.com/
ModelName : NETGEAR Orbi Desktop AC3000 Router
ModelNumber : RBR50
Description : WANConnectionDevice on NETGEAR RBR50 Orbi Router
ModelURL : http://www.netgear.com/
UPC : 606449084528
SerialNumber : 5R21945T06DA4
Services : System.__ComObject

同样地,也适用于 “ParentDevice” 和 `Services”。我们来查看我的 NetGear 设备的更多服务。这次,我希望直接存取我的 NetGear 设备(这样比枚举所有设备快得多)。不过您需要知道它的唯一设备名。在我的例子中,”UniqueDeviceName” 的值是 “uuid:4d696e69-444c-164e-9d42-3894ed0e1db5”:

1
2
3
4
5
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
# the UDN is unique, so you need to find out the UDN for your device first
# you cannot use the UDN I used
$myNetgearSwitch = $UPnPFinder.FindByUDN('uuid:4d696e69-444c-164e-9d42-3894ed0e1db5')
$myNetgearSwitch

这次,几乎立即识别出我的设备。要列出它的服务,我只需要获取它的 “Services” 属性,并且 PowerShell 自动将该 COM 对象转为可见的属性:

1
2
3
4
5
PS> $myNetgearSwitch.Services

ServiceTypeIdentifier Id LastTransportStatus
--------------------- -- -------------------
urn:schemas-upnp-org:service:Layer3Forwarding:1 urn:upnp-org:serviceId:L3Forwarding1 0

PowerShell 技能连载 - 探索即插即用设备(第 1 部分)

您可能已经生活在一个互联的智能家居,许多设备连接到您的网络。PowerShell 只需几行代码就可以帮助您找到您的设备:

1
2
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("upnp:rootdevice", 0)

请注意 UPnP 查找器组件需要一些时间来检测您的设备。结果看起来类似这样:

IsRootDevice     : True
RootDevice       : System.__ComObject
ParentDevice     :
HasChildren      : False
Children         : System.__ComObject
UniqueDeviceName : uuid:73796E6F-6473-6D00-0000-001132283f5e
FriendlyName     : Storage2 (DS414)
Type             : urn:schemas-upnp-org:device:Basic:1
PresentationURL  : http://192.168.2.107:5000/
ManufacturerName : Synology
ManufacturerURL  : http://www.synology.com/
ModelName        : DS414
ModelNumber      : DS414 5.1-5055
Description      : Synology NAS
ModelURL         : http://www.synology.com/
UPC              :
SerialNumber     : 001132283f5e
Services         : System.__ComObject

IsRootDevice     : True
RootDevice       : System.__ComObject
ParentDevice     :
HasChildren      : False
Children         : System.__ComObject
UniqueDeviceName : uuid:2f402f80-da50-11e1-9b23-001788ac0af1
FriendlyName     : BridgeOne (192.168.2.100)
Type             : urn:schemas-upnp-org:device:Basic:1
PresentationURL  : http://192.168.2.100/index.html
ManufacturerName : Signify
ManufacturerURL  : http://www.meethue.com/
ModelName        : Philips hue bridge 2015
ModelNumber      : BSB002
Description      : Philips hue Personal Wireless Lighting
ModelURL         : http://www.meethue.com/
UPC              :
SerialNumber     : 001788ac0af1
Services         : System.__ComObject
...

使用 Select-Object 来选择您感兴趣的属性:

1
2
3
4
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("upnp:rootdevice", 0) |
Select-Object ModelName, FriendlyName, PresentationUrl |
Sort-Object ModelName

在我的家中,该列表看起来类似这样:

ModelName                          FriendlyName               PresentationURL
---------                          ------------               ---------------
AFTMM                              Tobias's 2nd Fire TV
AFTS                               Tobias's Fire TV
AFTT                               Tobias's 3rd Fire TV stick
DS414                              Storage2 (DS414)           http://192.168.2.107:5000/
NETGEAR Orbi Desktop AC3000 Router RBR50 (Gateway)            http://www.orbilogin.net/
Philips hue bridge 2015            BridgeOne (192.168.2.100)  http://192.168.2.100/index.html
Philips hue bridge 2015            BridgeWork (192.168.2.106) http://192.168.2.106/index.html
SoundTouch 20                      Bad
SoundTouch 30                      Portable
SoundTouch SA-4                    Garden
SoundTouch SA-4                    LivingRoom

我正在使用群晖 NAS,假设我忘了访问它的 URL,那么现在可以快速地找回它。我的飞利浦灯光系统也是这样:UPnP 查找器返回所有 hub 的 IP 地址。

请注意 UPnP 查找器只能返回和您的计算机连入相同网络的设备。如果您的设备 IP 地址是在其它子网,那么不能通过这种方式检测。

PowerShell 技能连载 - 列出已安装的更新(第 2 部分)

在前一个技能中我们演示了如何通过 Windows Update 客户端获取当前已安装的更新。

可以对此列表进行润色,例如您可以使用哈希表创建计算的属性,提取默认情况下属于其他属性的信息,如知识库文章编号作为标题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$severity = @{
Name = 'Severity'
Expression = { if ([string]::IsNullOrEmpty($_.MsrcSeverity)) { 'normal' } else { $_.MsrcSeverity }}
}

$time = @{
Name = 'Time'
Expression = { $_.LastDeploymentChangeTime }
}

$kb = @{
Name = 'KB'
Expression = { if ($_.Title -match 'KB\d{6,9}') { $matches[0] } else { 'N/A' }}
}

$UpdateSession = New-Object -ComObject Microsoft.Update.Session
$UpdateSession.CreateupdateSearcher().Search("IsInstalled=1").Updates |
Select-Object $time, Title, $kb, Description, $Severity |
Out-GridView -Title 'Installed Updates'

结果显示在一个网格视图窗口中。如果您移除了 Out-GridView 那么信息看起来类似这样:

Time        : 9/10/2019 12:00:00 AM
Title       : 2019-09 Security Update for Adobe Flash Player for Windows 10 Version 1903 for
              x64-based Systems (KB4516115)
KB          : KB4516115
Description : A security issue has been identified in a Microsoft software product that could
              affect your system. You can help protect your system by installing this update
              from Microsoft. For a complete listing of the issues that are included in this
              update, see the associated Microsoft Knowledge Base article. After you install
              this update, you may have to restart your system.
Severity    : Critical

Time        : 10/8/2019 12:00:00 AM
Title       : Windows Malicious Software Removal Tool x64 - October 2019 (KB890830)
KB          : KB890830
Description : After the download, this tool runs one time to check your computer for infection by specific, prevalent malicious software (including Blaster, Sasser,
and Mydoom) and helps remove any infection that is found. If an infection is found, the tool will display a status report the next time that you start your computer. A new version of the tool will be offered every month. If you want to manually run the tool on your computer, you can download a copy from the Microsoft Download Center, or you can run an online version from microsoft.com. This tool is not a replacement for an antivirus product. To help protect your computer, you should use an antivirus product.
Severity    : normal

Time        : 10/8/2019 12:00:00 AM
Title       : 2019-10 Cumulative Update for .NET Framework 3.5 and 4.8 for Windows 10 Version
              1903 for x64 (KB4524100)
KB          : KB4524100
Description : Install this update to resolve issues in Windows. For a complete listing of the issues that are included in this update, see the associated Microsoft Knowledge Base article for more information. After you install this item, you may have to restart your computer.
Severity    : normal

Time        : 10/28/2019 12:00:00 AM
Title       : Update for Windows Defender Antivirus antimalware platform - KB4052623 (Version 4.18.1910.4)
KB          : KB4052623
Description : This package will update Windows Defender Antivirus antimalware platform’s components on the user machine.
Severity    : normal

...

PowerShell 技能连载 - 列出已安装的更新(第 1 部分)

Get-Hotfix 只会列出操作系统相关的 hotfix:

1
Get-HotFix

实际上,它只是一个 WMI 查询的简单包装,结果是一样的:

1
Get-CimInstance -ClassName Win32_QuickFixEngineering

一个更简单更完整的方法是查询系统事件日志获取所有安装的更新:

1
2
3
4
5
6
7
8
9
10
Get-WinEvent @{
Logname='System'
ID=19
ProviderName='Microsoft-Windows-WindowsUpdateClient'
} | ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeCreated
Update = $_.Properties.Value[0]
}
}

显然,当系统事件日志清除之后,查询结果就不完整了。此外,该日志只是记录任何更新安装,因此随着时间的推移,新的更新可能取代旧的更新。

要保证获取到完整的已安装更新列表,您需要请求 Windows Update 客户端,从实际安装的更新中重建列表,这要消耗更多的时间:

1
2
3
4
$result = (New-Object -ComObject Microsoft.Update.Session).CreateupdateSearcher().Search("IsInstalled=1").Updates |
Select-Object LastDeploymentChangeTime, Title, Description, MsrcSeverity

$result | Out-GridView -Title 'Installed Updates'

PowerShell 技能连载 - 退出 PowerShell 管道(第 2 部分:手动退出)

在前一个技能中我们学到了如何当达到一定次数的时候退出 PowerShell 管道,这样可以节约很多时间:

1
2
3
$fileToSearch = 'ngen.log'
Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue -Filter $fileToSearch |
Select-Object -First 1

显然,当一定数量的结果传递给 Select-Object 之后,Select-Object 会发送秘密的信息到上一级管道的 cmdlet,告知它们停止。实际上,Select-Object 会抛出一个特殊的异常,PowerShell 会处理这个异常,才产生这个魔术的效果。

但是如果事先不知道确切的结果数量呢?如果您希望在其它情况下中断管道呢?如果您希望实现某种超时呢?要手动退出一个管道,只需要让 Select-Object 发出这个特殊的异常。以下是一个专门做这件事的 Stop-Pipeline 函数:

1
2
3
4
5
6
7
function Stop-Pipeline
{
$pipeline = { Select-Object -First 1 }.GetSteppablePipeline()
$pipeline.Begin($true)
$pipeline.Process(1)
$pipeline.End()
}

它调用了一个 Select-Object 并且模仿在管道中执行。它是通过 GetSteppablePipeline() 实现的。您现在可以通过 Process() 人工传递数据给 Select-Object。由于通过 -First 1 参数执行 Select-Object 命令,所以当传递任何数据给 Select-Object,都会产生该特殊的异常。

您现在获得了控制权,并且可以通过任何条件来调用 Stop-Pipeline。以下示例程序将搜索文件并且在最长 2 秒之内退出管道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Stop-Pipeline
{
$pipeline = { Select-Object -First 1 }.GetSteppablePipeline()
$pipeline.Begin($true)
$pipeline.Process(1)
$pipeline.End()
}

# abort pipeline after 2000 milliseconds
$timeout = 2000
# create a stopwatch
$stopwatch = [system.diagnostics.stopwatch]::StartNew()

Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
if ($stopwatch.ElapsedMilliseconds -gt $timeout)
{
$stopwatch.Stop()
Write-Warning "Timeout, Pipeline Aborted."
# abort pipeline
Stop-Pipeline
}

# return the original object
$_
}

PowerShell 技能连载 - 退出 PowerShell 管道(第 1 部分:Select-Object)

有些时候人工退出 PowerShell 管道可以节约很多时间。例如,在开始递归搜索之前不能确切地知道一个文件所在的位置,当搜到文件的时候立刻停止。当找到文件以后没理由继续搜索其它目录。

下面是一个演示该问题的场景:假设您在 Windows 文件夹中的某个地方搜索一个名为 “ngen.log” 的文件:

1
2
3
$fileToSearch = 'ngen.log'

Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue -Filter $fileToSearch

PowerShell 将会查找这个文件但是找到之后还会继续搜索目录树的其它部分,这会消耗很多时间。

如果您知道需要查找结果的个数,那么可以当搜到指定数量的结果后使用 Select-Object 来立刻退出管道。

1
2
3
$fileToSearch = 'ngen.log'
Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue -Filter $fileToSearch |
Select-Object -First 1

在这个例子中,当找到文件时 PowerShell 立即退出。作为最佳实践,如果您事先知道需要从命令中查找多少个结果,那么在管道尾部添加 Select-Object,并且用 -First 参数来告诉 PowerShell 需要的结果个数。

PowerShell 技能连载 - 使用一个计时器来测量执行时间

有一些情况下您希望知道一段代码需要执行多长时间,例如返回统计或者对比代码,有许多方法可以测量命令,包括 Measure-Command cmdlet:

1
2
3
4
5
6
7
$duration = Measure-Command -Expression {
$result = Get-Hotfix
}

$time = $duration.TotalMilliseconds

'{0} results in {1:n1} milliseconds' -f $result.Count, $time

不过 Measure-Command 有一些不受人喜欢的副作用:

  • 所有输出都被丢弃,这样输出数据不会影响测量时间,而且您无法控制这个行为
  • 出于好几个原因,它会减慢您的代码执行,其中一个是 Measure-Command 在 dot-sourced 表达式中以一个独立的脚本快执行

所以,常常使用另一个技术,用的是 Get-Date,例如这样:

1
2
3
4
5
6
$start = Get-Date
$result = Get-Hotfix
$end = Get-Date
$time = ($end - $start).TotalMilliseconds

'{0} results in {1:n1} milliseconds' -f $result.Count, $time

它可以有效工作,不过也有一些不受欢迎的副作用:

  • 它产生更多的代码
  • 当计算机处于睡眠或休眠状态,将会影响结果,因为计算机关闭的时间不应该计入统计时间

一个更简洁的解决方案是使用 .NET 的 stopwatch 对象,它产生的代码更少,并且不会减缓代码的执行,而且不受睡眠或休眠的影响:

1
2
3
4
5
$stopwatch =  [system.diagnostics.stopwatch]::StartNew()
$result = Get-Hotfix
$time = $stopwatch.ElapsedMilliseconds

'{0} results in {1:n1} milliseconds' -f $result.Count, $time

此外,您可以对 stopwatch 对象调用 Stop()Restart()Reset() 方法。通过这些方法,您可以暂停测量代码中的某些部分(例如数据输出)并且继续测量。

PowerShell 技能连载 - Foreach -parallel (第 3 部分:批量 Ping)

在 PowerShell 7 中,带来了一个新的并行的 Foreach-Object,它可以并行执行代码并显著加快操作的速度。同样的技术可通过以下模块在 Windows PowerShell 中使用:

1
Install-Module -Name PSParallel -Scope CurrentUser -Force

我们来看看有趣的案例。例如,如果您需要获得一个能够响应 ping (ICMP) 的计算机列表,这将需要很长时间。一个加速操作的方法是使用带超时设置的 ping,这样无需对不响应的机器等待太久。

这段代码 ping powershell.one 并且等待最长 1000 毫秒(代码来自 https://powershell.one/tricks/network/ping):

1
2
3
4
5
6
$ComputerName = 'powershell.one'
$timeout = 1000
$ping = New-Object System.Net.NetworkInformation.Ping

$ping.Send($ComputerName, $timeout) |
Select-Object -Property Status, @{N='IP';E={$ComputerName}}, Address

现在让我们来加速整件事,整批 ping 整个 IP 段!

我们先来看看在 PowerShell 7 中如何实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#requires -Version 7.0

# IP range to ping
$IPAddresses = 1..255 | ForEach-Object {"192.168.0.$_"}

# timeout in milliseconds
$timeout = 1000

# number of simultaneous pings
$throttleLimit = 80

# measure execution time
$start = Get-Date

$result = $IPAddresses | ForEach-Object -ThrottleLimit $throttleLimit -parallel {
$ComputerName = $_
$ping = [System.Net.NetworkInformation.Ping]::new()

$ping.Send($ComputerName, $using:timeout) |
Select-Object -Property Status, @{N='IP';E={$ComputerName}}, Address
} | Where-Object Status -eq Success

$end = Get-Date
$time = ($end - $start).TotalMilliseconds

Write-Warning "Execution Time $time ms"

$result

在 5 秒钟左右,整个 IP 段都 ping 完毕,并且返回会 ICMP 请求的 IP 地址。

在 Windows PowerShell 中,这段代码基本差不多(假设您已从 PowerShell Gallery 中安装了 PSParallel 模块):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#requires -Modules PSParallel
#requires -Version 3.0

# IP range to ping
$IPAddresses = 1..255 | ForEach-Object {"192.168.0.$_"}

# number of simultaneous pings
$throttleLimit = 80

# measure execution time
$start = Get-Date

$result = $IPAddresses | Invoke-Parallel -ThrottleLimit $throttleLimit -ScriptBlock {
$ComputerName = $_
# timeout in milliseconds
$timeout = 1000

$ping = New-Object -TypeName System.Net.NetworkInformation.Ping

$ping.Send($ComputerName, $timeout) |
Select-Object -Property Status, @{N='IP';E={$ComputerName}}, Address
} | Where-Object Status -eq Success

$end = Get-Date
$time = ($end - $start).TotalMilliseconds

Write-Warning "Execution Time $time ms"

$result

ForEach-Object 不同,代码使用的是 Invoke-Parallel,而且由于 Invoke-Parallel 不支持 “use:“ 前缀,所以所有局部变量都必须包含在脚本块中(在我们的示例中,是变量$timeout)。

Invoke-Parallel 支持一个友好的进度条,这样您可以知道有多少个任务正在并行执行。

PowerShell 技能连载 - Foreach -parallel (第 2 部分:Windows PowerShell)

PowerShell 7 发布了一个内置参数,可以并行运行不同的任务:

1
1..100 | ForEach-Object -ThrottleLimit 20 -Parallel { Start-Sleep -Seconds 1; $_ }

如果您使用的是 Windows PowerShell,那么您也可以使用类似的并行技术。例如,下载并安装这个免费的模块:

1
Install-Module -Name PSParallel -Scope CurrentUser -Force

它带来一个新的命令:Invoke-Parallel:您可以这样使用它:

1
1..100 | Invoke-Parallel -ThrottleLimit 20 -Scrip-tBlock { Start-Sleep -Seconds 1; $_ }

由于 Invoke-Parallel 使用的是和 PowerShell 7 相同的技术,所以有相同的限制:每个线程都在它自己的线程中执行,并且不能存取本地变量。在下一个技能中,我们将学习一些有趣的示例。

PowerShell 技能连载 - Foreach -parallel(第 1 部分:PowerShell 7)

PowerShell 7 发布了一个内置参数,可以并行运行不同的任务。以下是一个简单示例:

1
1..100 | ForEach-Object -ThrottleLimit 20 -Parallel { Start-Sleep -Seconds 1; $_ }

在普通的 ForEach-Object 循环中,这将花费 100 秒的时间执行。如果使用了 parallel,代码可以并行地执行。-ThrottleLimit 定义了“块”,因此在本例中,有20个线程并行运行,使总执行时间减少到5秒。

在过于激动之前,请记住每个线程都在其自己的 PowerShell 环境中运行。幸运的是,您可以访问前缀为“using:”的局部变量:

1
2
3
$text = "Output: "

1..100 | ForEach-Object -ThrottleLimit 20 -Parallel { Start-Sleep -Seconds 1; "$using:text $_" }

不过,当您开始使用多线程时,您需要了解线程安全知识。复杂对象,例如 ADUser 对象可能无法在多个线程之间共享,所以需要每个案例独立判断是否适合使用并行。

由于并行的 ForEach-Object 循环内置在 PowerShell 7 中,这并不意味着可以在 Windows PowerShell 中使用并行。在 Windows PowerShell 中有许多模块实现了该功能。我们将会在接下来的技能中介绍它们。