「对 200 台电脑逐一 ping 以确认存活状态的脚本,跑完一轮要花 15 分钟」「共享文件夹下 10 万个文件的哈希计算迟迟算不完」── PowerShell 的自动化发展到一定规模后,必然会撞上处理时间这堵墙。而这类处理大多数情况下都是等待时间占主导。CPU 明明是空闲的,却因为要逐一按顺序等待网络响应或磁盘 I/O 而变慢。这正是并行化该出场的地方。
PowerShell 7 中可以使用 ForEach-Object -Parallel 了,并行处理的门槛因此大幅降低。不过,如果草率地并行化,不仅不会变快,反而会变慢,甚至还会导致结果出错,这一点也很棘手。「更新了共享变量,值却飞了」「输出的顺序每次都不一样」「找不到调用方定义的函数」,这些都是在不了解并行处理机制的情况下使用时的典型症状。
本文将系统整理 PowerShell 中可用的并行化手段,从 ForEach-Object -Parallel 的用法与陷阱,到与 Start-ThreadJob / Start-Job 的选用之道,乃至”不应该并行化的情形”,都以能在实务中做出判断的形式加以梳理。
1. 先说结论
ForEach-Object -Parallel是在 PowerShell 7.0 中新增的。Windows PowerShell 5.1 中不存在这个功能。1- 各个脚本块是在各自独立的 Runspace(执行环境)中运行的。调用方的变量、函数原样是看不到的。变量要通过
$using:作用域修饰符传递。1 -ThrottleLimit的默认值是 5。PowerShell 7.1 及以后会复用 Runspace 池,ThrottleLimit 就是该池的大小。如果想每次都重新创建,用-UseNewRunspace。1- 通过
$using:传递的是”引用”。只读取是安全的,但如果要更新,就需要使用System.Collections.Concurrent下的线程安全类型。普通哈希表或 List 的并发更新会遭到破坏。1 - 并行化并不是”一定会变快”的举措。官方文档也明确写明,如果把琐碎的处理并行化,速度可能会比通常慢得多。真正有效的是等待时间长的处理,以及在多核上有意义的计算处理。
- 输出和错误的顺序都是不确定的。写入错误流的顺序也是随机的,脚本块内的终止性错误只会终止该次迭代,并以
PSTaskException的形式报告。1 - 通过
-AsJob可以作为作业投递出去。不过-ThrottleLimit是每个作业的并行数,如果创建多个作业,同时执行数就会是相乘的结果。1 - 轻量并行用 Start-ThreadJob,需要隔离用 Start-Job。Start-Job 是在独立的进程中运行的,因此开销较大,结果会经过序列化,返回时会失去方法。23
- 如果要对多台 Windows 下发相同的处理,
Invoke-Command隐含的并行执行是最快的方式。它默认会同时对最多 32 台执行。4
2. 并行化的四种方案
2.1 前提知识 ── 什么是 Runspace(运行空间)
本文中会反复出现的”Runspace”,先在这里说明清楚。Runspace 是指 PowerShell 用于执行脚本的”执行环境”本身。变量、函数、已加载的模块、当前目录这些会话状态的整体,都属于这个 Runspace。
如果从进程、线程的关系来说,是这样的。
| 层级 | 是什么 | 在本文中的含义 |
|---|---|---|
| 进程 | 操作系统视角下的执行单位(pwsh.exe 这样的一个实例) |
一个进程中可以拥有多个 Runspace |
| 线程 | 实际运行代码的执行流 | Runspace 是在线程之上运行的 |
| Runspace | PowerShell 状态(变量、函数、模块)的容器 | 一旦分开,变量和函数都不会共享 |
大致可以理解成:”在一个进程中,可以启动多个各自拥有独立变量和函数的 PowerShell 会话“。ForEach-Object -Parallel 就是准备多个这样的 Runspace,让各个脚本块分别在不同线程上运行的机制。1
本文中的大部分麻烦事,几乎都是从这里产生的。调用方的变量或函数属于调用方的 Runspace,从别的 Runspace 是看不到的。所以变量必须通过 $using: 明确传递,函数则必须模块化后在各个 Runspace 中加载。而与此同时,由于进程是相同的,内存中对象的实体是可以共享的。这一点,会以后文所述的线程安全性问题的形式反过来出现。
“看不见,却又是共享的”── 这种乍看之下矛盾的性质,正是在不理解 Runspace 的情况下贸然并行化时引发事故的根源。
2.2 四种手段
先建立一张地图。PowerShell 中”同时运行”的手段大体可以分为四种。
| 手段 | 执行单位 | 可用版本 | 适用场景 |
|---|---|---|---|
ForEach-Object -Parallel |
线程(Runspace) | PowerShell 7.0+1 | 对集合中的每个元素做相同的处理。第一候选 |
Start-ThreadJob |
线程(Runspace) | PowerShell 7 内置 / 5.1 需从 Gallery 引入2 | 把少量处理投递出去,之后再回收 |
Start-Job |
独立进程 | 5.1、7 都是标准功能3 | 需要隔离,或希望放到独立进程中执行的处理 |
Invoke-Command -ComputerName |
远程的各台电脑 | 5.1、7 都是标准功能4 | 向多台 Windows 下发相同的处理 |
实务上的选用方法很简单。如果是对大量对象应用相同处理,用 ForEach-Object -Parallel;如果对象是”多台远程电脑”,则优先考虑远程执行。后者会默认对指定的多台计算机同时执行,最多 32 台,因此不需要自己编写并行化代码。4 远程执行本身的配置方法,请参考《PowerShell Remoting(WinRM)入门 ── 批量管理多台 Windows》。
Start-Job 之所以开销较大,是因为后台作业是作为独立的进程启动的。除了进程启动本身的成本以外,结果还会经过序列化后返回,所以收到的对象会变成不带方法的”反序列化副本”。3 相对地,Start-ThreadJob 是在同一进程内的线程中运行的,因此大幅轻量化。2
3. ForEach-Object -Parallel 的基本用法
最简单的形式是这样的。$_ 是当前的输入对象,外部的变量需要加上 $using: 才能引用。1
$timeout = 2 # 调用方的变量
$result = $computers | ForEach-Object -Parallel {
$name = $_
# 外部的变量必须加上 $using: 才看得见
$ok = Test-Connection -TargetName $name -Count 1 -TimeoutSeconds $using:timeout -Quiet
# 不要更新用于汇总的共享变量,而是"输出"结果,这是基本形式
[pscustomobject]@{
Computer = $name
Alive = $ok
CheckedAt = Get-Date
}
} -ThrottleLimit 20
$result | Where-Object { -not $_.Alive } | Format-Table
这里有一条应该牢记的设计原则。不要更新共享变量,而是让每个脚本块”输出”结果,由调用方统一接收。只要保持这种形式,线程安全性的问题就根本不会发生。在并行处理中出问题的代码,大多都是在试图改写共享状态。
如果无论如何都想汇总到一个共享集合中,就要使用线程安全的类型。1
# 官方示例的模式: ConcurrentDictionary 可以被多个线程安全地更新
$safeDict = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
Get-Process | ForEach-Object -Parallel {
$dict = $using:safeDict
# TryAdd() 返回的是布尔值。不丢弃的话,True/False 就会混入输出中
$null = $dict.TryAdd($_.ProcessName, $_)
}
# 【危险】把普通的 Hashtable 或 List<T> 通过 $using: 传入并更新,是不安全的
# $shared = @{} # 并发更新会破坏内部结构,引发异常或数据丢失
这里要把 $using: 的含义弄准确。官方文档针对 ForEach-Object -Parallel 明确指出,Using: 修饰符会”把调用该命令的线程中的变量引用,传递到正在运行的各个脚本块所在的线程”。传递过去的是同一进程内、同一对象的引用。正因如此,只读取不发生变化的值是安全的,而要改写状态则需要线程安全的类型。1
而且,这种”传递的是引用”的性质,仅限于在同一进程内运行的并行化。Start-Job 是在独立进程中运行的,Invoke-Command -ComputerName 是在另一台机器上运行的,因此通过 $using: 传入的值会经过序列化,收到的是副本。在对方那边改写也不会反映到调用方,返回来的对象同样是没有方法的反序列化副本。3 虽然写法同样是 $using:,但含义不同,请不要把 ForEach-Object -Parallel 的感觉原样带入 Start-Job。
4. 五个陷阱
(1) 看不到调用方的函数
由于并行脚本块是在独立的 Runspace 中执行的,调用方定义的函数原样是无法调用的。正规做法是把公共处理模块化,在脚本块开头导入。
$items | ForEach-Object -Parallel {
Import-Module 'D:\Scripts\Modules\KsOps' -ErrorAction Stop # 在各个 Runspace 中加载
Convert-KsRecord -Input $_
} -ThrottleLimit 8
不过每个 Runspace 加载模块的成本,会随并行数成倍产生。在使用较重的模块进行处理时,即便提高并行度效果也会遇到瓶颈,这往往正是主要原因。模块化的方法整理在《PowerShell 脚本的参数设计与模块化》中。
(2) 由于 Runspace 会被复用,状态有时会被延续下去
在 PowerShell 7.0 中,每次迭代都会创建新的 Runspace,但从 7.1 开始,默认会从 Runspace 池中复用。1 这在性能上是很大的改进,但也意味着某次迭代中设置的环境变量、当前目录的更改、已加载模块的状态,会被使用同一 Runspace 的后续迭代看到。如果需要迭代之间完全独立,应指定 -UseNewRunspace(相应地会变慢)。1
(3) 输出和错误的顺序都不保证
并行执行的顺序是非确定性的。写入错误流的顺序也是随机的,警告、详细、信息流同样如此。1
1..5 | ForEach-Object -Parallel {
if ($_ -eq 3) { throw "Terminating Error: $_" } # 只有这次迭代会终止
"Output: $_"
}
# Output: 1 / Output: 4 / Output: 2 / Output: 5 (顺序不定),Output: 3 不会出现
脚本块内的终止性错误只会终止该次迭代,其他并行执行会继续。错误会以 FullyQualifiedErrorId 为 PSTaskException 的 ErrorRecord 形式写入错误流。这不是”只要有一件失败就全部停止”的行为,因此需要自己汇总全部处理的成败。1
$results = $files | ForEach-Object -Parallel {
# 在 catch 块内部,$_ 会变成 ErrorRecord,
# 因此输入对象必须先转存到别的变量里再使用
$file = $_
try {
$hash = Get-FileHash -Path $file.FullName -Algorithm SHA256 -ErrorAction Stop
[pscustomobject]@{ Path = $file.FullName; Hash = $hash.Hash; Error = $null }
}
catch {
# 失败也作为"输出"返回,由调用方分类处理
[pscustomobject]@{ Path = $file.FullName; Hash = $null; Error = $_.Exception.Message }
}
} -ThrottleLimit 8
$failed = $results | Where-Object Error
if ($failed) { Write-Warning "$($failed.Count) 件失败了" }
(4) PipelineVariable 无法使用
通用参数中的 -PipelineVariable,即便加上 $using:,在并行场景中也不受支持。1 这是从顺序处理移植代码时容易踩坑的一点。
(5) 进度显示与日志会混在一起
多个线程同时写入 Write-Progress 或自定义日志时,行会混在一起,文件也可能发生争用。日志不应在脚本块内部直接追加写入文件,而应当作为结果对象返回,由调用方在单一位置统一写入,这样才安全。输出流的设计方式在《PowerShell 的输出流与日志设计》中有详细讨论。
5. ThrottleLimit 的确定方法
-ThrottleLimit 是同时运行的脚本块数量,默认是 5。1 7.1 及以后,这个值就直接等于Runspace 池的大小。1 用图来表示这个动作,如下所示。
flowchart LR
IN["输入 200 件"] --> Q["等待队列<br/>等待空位出现"]
Q --> POOL
subgraph POOL["Runspace 池 ThrottleLimit = 5"]
R1["Runspace 1"]
R2["Runspace 2"]
R3["Runspace 3"]
R4["Runspace 4"]
R5["Runspace 5"]
end
POOL --> OUT["输出<br/>顺序不定"]
POOL -.->|"每处理完一件<br/>就复用同一个 Runspace"| Q
要点在于,不是为输入的 200 件各自创建 200 个 Runspace,而是复用 -ThrottleLimit 指定数量准备好的 Runspace。由此可以推导出两点:一是提高 -ThrottleLimit 会增加内存和初始化成本;二是正因为会被复用,前一次迭代的状态有可能残留下来(第 4 章 (2))。
确定数值的经验法则,按处理的性质来区分。
| 处理的性质 | 大致数值 | 理由 |
|---|---|---|
| 网络等待(连通性确认、API 调用) | 可以比核心数更大(从 20~50 左右开始尝试) | CPU 基本处于空闲状态。制约来自对方 |
| 磁盘 I/O 等待 | 从 8~16 左右开始尝试 | 设得太大会增加随机访问,反而适得其反。SSD/HDD 差异较大 |
| CPU 计算(哈希、压缩、转换) | 大致等于逻辑核心数 | 超过这个数只会造成上下文切换的浪费 |
| 对方是业务服务器・API | 以对方的承受能力为上限 | 超过速率限制或最大并发连接数,就会变成加害者一方 |
最后一行在实务中最为重要。为了让自己的脚本变快而把业务服务器搞垮,就是本末倒置,因此在面对公司内部 API 或文件服务器时,自身的并行度应当以”对方能承受的范围”来确定。关于 API 速率限制的应对方式,请参考《用 PowerShell 对接 REST API》。
使用 -AsJob 时的注意事项,官方文档也有明确说明。ThrottleLimit 是每次 ForEach-Object -Parallel 调用的限制,如果创建 10 个作业,就会有”10 × ThrottleLimit”同时运行。1
还有一点,即便同样叫 -ThrottleLimit,不同命令所计数的对象也不一样。混淆的话会导致并行度的估算出错,本文中出现的这两个放在一起对比一下。
| 命令 | -ThrottleLimit 计数的对象 |
默认值 |
|---|---|---|
ForEach-Object -Parallel |
同时运行的脚本块数量(= Runspace 池的大小) | 51 |
Invoke-Command -ComputerName |
同时连接的计算机台数 | 324 |
也就是说,Invoke-Command -ThrottleLimit 32 的含义是”同时连接 32 台”,而不是”每台以 32 并行处理”。如果两者组合使用(在远程端运行 ForEach-Object -Parallel),也请注意台数 × 并行度才是对方的总负载。
6. 不并行化反而更快的情形
官方文档写得相当直白 ── 新建 Runspace 相比顺序处理有相当大的开销,如果并行脚本处理的是琐碎的工作,速度可能会比通常慢得多,要实际测试找出有效果的场景。1 官方示例中甚至有明确标注为”这是并行化的低效使用示例”的样例。
判断标准很简单。
- 单件处理时间短(毫秒量级) → 不并行化。字符串处理、哈希表查找这类操作,顺序处理更快
- 数量少(几十件) → 不并行化。开销相对更大
- 单件有数百毫秒以上的等待 → 并行化有价值
- 有长时间占用 CPU 的计算 → 在多核上有效果
而且一定要先测量再决定。用 Measure-Command 对比顺序版和并行版,几十秒就能得出答案。测量的方法以及 PowerShell 脚本加速的整体思路,整理在《PowerShell 脚本变慢时该看哪里》中。
# 用相同输入对比顺序和并行版本(多运行几次,观察稳定后的数值)
# 并行一侧是在独立的 Runspace 中运行的,看不到调用方定义的函数。
# 为了公平比较,要么加载模块,要么直接把处理写在脚本块里
$seq = Measure-Command {
$items | ForEach-Object { Invoke-Work $_ }
}
$par = Measure-Command {
$items | ForEach-Object -Parallel {
Import-Module 'D:\Scripts\Modules\KsOps' # ← 没有这一行会导致命令未找到
Invoke-Work $_
} -ThrottleLimit 16
}
'{0:N1}秒 → {1:N1}秒' -f $seq.TotalSeconds, $par.TotalSeconds
7. Start-ThreadJob 与 Start-Job 的选用之道
ForEach-Object -Parallel 适合”对大量输入应用相同处理”的场景,但如果想同时运行几个性质不同的处理,之后再回收结果,就更适合用作业。
# 同时运行多个不同的处理,最后统一等待(线程作业 = 轻量)
# 线程作业也是在独立的 Runspace 中运行的,看不到调用方的函数。
# 在每个作业开头加载模块(或者写成自包含的脚本块)
$jobs = @(
Start-ThreadJob -Name 'AD用户盘点' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsAdUserReport
}
Start-ThreadJob -Name '文件服务器容量' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsShareUsage
}
Start-ThreadJob -Name '许可证汇总' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsLicenseSummary
}
)
# 等待完成并回收结果。错误一定要用 -ErrorVariable 接收
$reports = $jobs | Receive-Job -Wait -ErrorAction SilentlyContinue -ErrorVariable jobErrors
# 状态变为 Failed,仅限于脚本块因"终止性错误"而中断的情况。
# 以 Write-Error 这类非终止性错误结束的作业,状态依旧是 Completed,
# 如果只看状态,就会出现"明明有错误却被当作成功"的情况
$failed = @($jobs | Where-Object { $_.State -eq 'Failed' })
$jobs | Remove-Job -Force
if ($failed.Count -gt 0 -or @($jobErrors).Count -gt 0) {
throw "并行执行中发生了错误: $(($jobErrors | ForEach-Object { $_.Exception.Message }) -join ' / ')"
}
这里不使用 -AutoRemoveJob,是因为一旦回收完毕作业就会立即消失,无法确认到底是哪个失败了。Receive-Job 的错误默认是非终止性错误,如果什么都不写,就会变成”明明有一部分失败了,却只返回了成功的部分、看起来像是全部完成了”这种最麻烦的情况。在盘点或报表自动化中,这种遗漏会作为数字上的错误悄悄留存下来。
选择 Start-Job(进程隔离型)的场景如下。3
- 不想连累调用方进程的处理(比如调用可能崩溃的原生 DLL)
- 需要独立进程环境的处理(想以不同的区域设置或环境变量运行)
- 需要把执行环境本身区分开的处理,比如 32 位 / 64 位
反过来说,如果不属于这些情况,Start-Job 就会因为开销和序列化的限制而处于不利地位。反序列化后的对象没有方法这一点,在后续处理中也会造成影响。3
8. 只有 Windows PowerShell 5.1 的环境中
在 5.1 中无法使用 -Parallel。现实中的选择有三个。
Start-ThreadJob(引入 PowerShell Gallery 的ThreadJob模块)── 在 5.1 中也能使用轻量并行。公司内部分发的方法请参考《PowerShell 模块的公司内部分发与更新》2Invoke-Command -ComputerName── 如果对象是多台 Windows,仅凭这个就能实现并行(默认同时 32 台)4- 引入 PowerShell 7 ── 5.1 与 7 可以共存,只让较重的处理在 7 上运行,也是现实可行的选择
第 1 项的引入方法如下。在原生的 5.1 上直接 Install-Module 有时会失败,因此把容易踩坑的两点一并写出来。
# 【1】PowerShell Gallery 要求 TLS 1.2 以上。
# Windows PowerShell 5.1 默认没有启用,
# 有时会因"基础连接已关闭"等错误而失败。
# 在本次会话中临时启用 TLS 1.2 后再执行
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# 【2】引入。如果是 CurrentUser 则不需要管理员权限
Install-Module ThreadJob -Scope CurrentUser
# 【3】确认
Import-Module ThreadJob
Get-Command -Module ThreadJob # 出现 Start-ThreadJob 即表示引入成功
容易踩坑的地方有以下两点。
- TLS 1.2 ── PowerShell Gallery 自 2020 年 4 月起,要求以 TLS 1.2 以上进行连接。在 5.1 中有时需要上面那一行代码5
- 存储库的信任与执行策略 ── PSGallery 默认被当作”不受信任的存储库”处理,因此
Install-Module会要求确认。如果是无人值守引入,用-Force;如果是长期使用,则通过Set-PSRepository -Name PSGallery -InstallationPolicy Trusted一次性将其设为受信任。另外,如果执行策略是Restricted(Windows 客户端的默认值),在加载脚本时会被卡住,需要设置相当于Set-ExecutionPolicy -Scope CurrentUser RemoteSigned的配置。请避免将其永久设为Unrestricted6
第 3 项往往最终反而是最省事的方案,本公司也推荐”与其为了并行化在 5.1 上编写复杂代码,不如只让该处理在 7 上运行”。共存与迁移的思路整理在《Windows PowerShell 5.1 与 PowerShell 7 的区别 ── 公司内部脚本迁移实务指南》中。
9. 实务定式(判断表)
| 想做的事 | 选择 | 补充说明 |
|---|---|---|
| 对大量对象做相同处理(连通性确认、哈希计算、API 调用) | ForEach-Object -Parallel |
仅限 PowerShell 7。设计成结果以输出返回1 |
| 多台 Windows 上执行相同处理 | Invoke-Command -ComputerName |
默认同时 32 台。不需要自己写并行化4 |
| 同时执行性质不同的处理 | Start-ThreadJob |
轻量。用 Receive-Job -Wait 回收2 |
| 需要进程隔离 | Start-Job |
较重。结果会被反序列化3 |
| 想把结果汇总到一处 | 以输出返回 / ConcurrentDictionary |
普通 Hashtable、List 的并发更新不可行1 |
| 单件处理轻、数量少 | 不并行化 | 开销反而会导致变慢1 |
| 对方是业务服务器・API | 以对方为基准确定 ThrottleLimit | 速率限制、最大并发连接数是实际上限 |
| 迭代之间必须相互独立 | -UseNewRunspace |
避免因复用导致状态延续(会变慢)1 |
10. 总结
ForEach-Object -Parallel是 PowerShell 7.0 及以后的功能,会在各自独立的 Runspace 中执行每个脚本块。调用方的变量用$using:传递,函数通过模块化加载,这是基本形式。- 不要更新共享变量,而是把设计改成每次迭代都输出结果,由调用方汇总,这样几乎就能避免线程安全性的问题。如果无论如何都要共享,就使用 Concurrent 系列的类型。
- 默认的 ThrottleLimit 是 5。等待时间占主导的处理可以设得大一些,CPU 计算大致设为核心数左右,有对手方的处理则以对方的承受能力为上限。
- 输出、错误的顺序都不确定,终止性错误只会终止该次迭代。全部处理的成败需要自己汇总。
- 并行化并非万能。处理较轻或数量较少时,会因开销而变慢。请务必先用
Measure-Command与顺序版本比较后再采用。 - 在 5.1 环境中用
Start-ThreadJob,涉及远程用Invoke-Command,如果这些仍然不够,现实的做法是考虑引入 PowerShell 7。
示例代码下载
本文中涉及的代码,已整理成可以直接运行的形式提供下载。其中包含 ForEach-Object -Parallel / Start-ThreadJob 面向实务的常用范式。
本文的示例已在 PowerShell 7.6 中实际运行验证(Pester 8 项)。运行 zip 中包含的 Invoke-SampleTests.ps1,即可在您本地重现相同的验证。
# 语法解析 + 静态分析 + Pester 测试
./Invoke-SampleTests.ps1
设置值(路径、服务器名、租户 ID 等)仅为示例。请勿直接在生产环境中运行,请根据贵公司的环境自行替换。
相关文章
- Windows PowerShell 5.1 与 PowerShell 7 的区别 ── 企业内部脚本迁移实务指南
- PowerShell Remoting(WinRM)入门 ── 批量管理多台 Windows
- PowerShell 脚本的参数设计与模块化 ── 从「能运行的脚本」到「可以交给别人的脚本」
- 用 PowerShell 盘点文件服务器 ── 容量调查与访问权限(ACL)审计
- PowerShell 的错误处理与重试设计 ── 从 try/catch 失效的陷阱到 exit code、重试的实务定式
相关咨询领域
小村软件有限公司承接耗时较长的运维脚本提速、包含并行处理在内的自动化设计评审,以及”并行化之后结果变得奇怪了”这类故障的调查。
参考链接
-
Microsoft Learn,ForEach-Object。关于 PowerShell 7.0 中新增了 -Parallel 参数集,各脚本块在新的 Runspace 中执行,通过 $using: 作用域修饰符传递变量,-ThrottleLimit 的默认值为 5 且等于 Runspace 池大小,7.1 及以后 Runspace 会被复用、可通过 -UseNewRunspace 强制新建,-AsJob 与 -TimeoutSeconds 的行为,通过 $using: 传入的引用如需更新则需要 System.Collections.Concurrent 这类线程安全类型,非终止性错误的输出顺序不确定,终止性错误只会终止各自的并行实例且 FullyQualifiedErrorId 为 PSTaskException,PipelineVariable 在并行场景中不受支持,新建 Runspace 的开销较大、琐碎处理反而可能变慢,以及使用 -AsJob 时 ThrottleLimit 是每个作业各自的限制。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25
-
Microsoft Learn,Start-ThreadJob。关于 Start-ThreadJob 不是在独立进程、而是在同一进程内独立的线程中执行脚本块,因此比 Start-Job 更轻量,可以用标准的作业命令(Receive-Job 等)操作,以及通过 -ThrottleLimit 控制并发数。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn,about_Jobs。关于后台作业会在新进程中异步执行命令,通过 Start-Job、Get-Job、Receive-Job、Wait-Job 操作作业,以及作业的结果会经过序列化后返回。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn,Invoke-Command。关于可以通过 -ComputerName 指定多台计算机执行相同命令,-ThrottleLimit 用于限制并发连接数且默认值为 32,以及通过 -AsJob 实现后台执行。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft PowerShell Team Blog,PowerShell Gallery TLS Support。关于自 2020 年 4 月起 PowerShell Gallery 将 TLS 1.2 设为默认,客户端一侧也需要以 TLS 1.2 以上连接,以及在 Windows PowerShell 5.1 中,可通过设置
[Net.ServicePointManager]::SecurityProtocol为 Tls12 后再连接作为规避方案。 ↩ -
Microsoft Learn,about_Execution_Policies。关于执行策略是控制脚本加载条件的机制,在所有作用域均未设定时,Windows 客户端的有效策略为 Restricted、Windows Server 为 RemoteSigned,Restricted 下无法执行包括 .ps1、.psm1 在内的脚本文件,以及可以通过指定 -Scope 只应用于当前用户或进程。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
PowerShell脚本运行慢时该看哪里 ── 数组・管道・匹配的诀窍
整理 PowerShell 脚本变慢的常见原因。从数组 += 导致 O(n^2) 的原理、管道与 foreach 的差异、匹配的哈希表化、文件 I/O 的改善,到正确的测量方法,从实务角度进行讲解。
不再使用 Write-Host ── PowerShell 的输出流与日志设计
本文整理 PowerShell 六个输出流的用法区分、Write-Host 存在的问题与正确的使用场景、函数返回值被污染的原因、通过 -Verbose 与 -InformationVariable 实现调用方控制,以及结构化日志的留存方法。
从 PowerShell 正确调用外部 exe ── 参数引用、退出代码与乱码的陷阱
从 PowerShell 调用 robocopy 或公司内部 EXE 时,参数会损坏、拿不到退出代码、输出还会出现乱码。本文从实务角度整理 PowerShell 7.3 的参数传递变更、停止解析令牌 --%、以及 Start-Process 的使用区分。
PowerShell 中凭据的安全处理方式 ── 把明文密码逐出脚本
本文整理将 PowerShell 脚本中的明文密码迁移到安全存储的步骤。讲解 SecureString 的真实面貌与局限、Export-Clixml 基于 DPAPI 保存的原理,直至 SecretManagement/SecretStore 的适用场景。
PowerShell 脚本的参数设计与模块化 ── 从「能运行的脚本」到「可以交给别人的脚本」
本文整理把 PowerShell 脚本提升到可以交给他人使用的品质的具体步骤,讲解 param 块与 [CmdletBinding()]、输入验证、管道输入、-WhatIf 支持、.psm1 模块化,直至内部共享与 Git 管理的要点。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
常见问题
汇总了咨询这一主题时常见的问题。
- ForEach-Object -Parallel 在 Windows PowerShell 5.1 中也能用吗?
- 不能。-Parallel 是 PowerShell 7.0 中新增的参数集,5.1 中根本不存在这个功能。如果要在 5.1 中并行化,可以选择从 PowerShell Gallery 引入的 ThreadJob 模块中的 Start-ThreadJob、标准的 Start-Job(进程隔离型,开销较大),或者自己搭建 RunspacePool。如果目的是加快公司内部脚本的速度,与其在 5.1 上编写复杂的并行处理代码,不如引入 PowerShell 7 并使用 -Parallel,在可维护性上也更有优势。
- 并行化之后反而变慢了,这是为什么?
- 因为并行执行的开销比处理本身还大。ForEach-Object -Parallel 会在各自独立的 Runspace 中执行每个脚本块,相较于顺序处理有相当大的开销,官方文档也明确写明"如果并行脚本处理的是琐碎的工作,速度可能会比通常慢得多"。如果单件处理只需几毫秒就能完成,或者数量只有几十件,通常不并行化反而更快。真正有效果的场景是网络等待或文件 I/O 等待较长的处理,以及在多核上有意义的计算处理。
- 可以从并行脚本块内部,向通过 $using: 传入的变量写入值吗?
- 读取所引用的值是安全的,但除非目标是线程安全的类型,否则写入并不安全。$using: 是把变量的引用从调用方线程传递给各脚本块所在线程的机制,由于会有多个线程同时访问,如果更新的是普通的哈希表或 List,就会遭到破坏。如果想汇总统计结果,请使用 System.Collections.Concurrent 命名空间下 ConcurrentDictionary、ConcurrentBag 之类的线程安全类型,或者干脆把设计改成由各脚本块输出值、再由调用方接收。
- ThrottleLimit 应该设成多少才正确?
- 这取决于处理的性质。默认值是 5。以网络等待或文件 I/O 等待为主导的处理(如向服务器确认连通性、调用 API 等),即便设置得比 CPU 核心数更大,也能看到效果。另一方面,在把 CPU 用满的计算处理中,一旦超过核心数左右,只会因为争用而变慢。如果对方是业务服务器或 API,除了自身的诉求之外,对方的最大并发连接数或速率限制也会成为上限。稳妥的做法是先用默认值 5 测量,再翻倍看看是否有效果。
- 无法从并行脚本块内部调用自己写的函数。
- 因为并行脚本块是在与调用方不同的 Runspace 中执行的,所以在调用方作用域中定义的函数或变量,原样是看不到的。有两种应对方法:一是把公共处理做成模块(.psm1),在脚本块开头 Import-Module;二是把函数定义当作文本通过 $using: 传入,在脚本块内部重新定义。从可维护性的角度看,推荐前者。不过要注意的是,在每个 Runspace 中加载模块都要花费成本,如果模块较重,即使提高并行度效果也会遇到瓶颈。