PowerShell 的并行处理 ── ForEach-Object -Parallel 与作业(Job)的选用之道

· · PowerShell, Windows, 并行处理, 性能改善, 自动化, 运维改善, 脚本, 作业

「对 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 就是该池的大小。如果想每次都重新创建,用 -UseNewRunspace1
  • 通过 $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 不会出现

脚本块内的终止性错误只会终止该次迭代,其他并行执行会继续。错误会以 FullyQualifiedErrorIdPSTaskException 的 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 用图来表示这个动作,如下所示。

每处理完一件就复用同一个 RunspaceRunspace 池 ThrottleLimit = 5Runspace 1Runspace 2Runspace 3Runspace 4Runspace 5输入 200 件等待队列等待空位出现输出顺序不定

要点在于,不是为输入的 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。现实中的选择有三个。

  1. Start-ThreadJob(引入 PowerShell Gallery 的 ThreadJob 模块)── 在 5.1 中也能使用轻量并行。公司内部分发的方法请参考《PowerShell 模块的公司内部分发与更新2
  2. Invoke-Command -ComputerName ── 如果对象是多台 Windows,仅凭这个就能实现并行(默认同时 32 台)4
  3. 引入 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 面向实务的常用范式。

下载示例代码(zip)

本文的示例已在 PowerShell 7.6 中实际运行验证(Pester 8 项)。运行 zip 中包含的 Invoke-SampleTests.ps1,即可在您本地重现相同的验证。

# 语法解析 + 静态分析 + Pester 测试
./Invoke-SampleTests.ps1

设置值(路径、服务器名、租户 ID 等)仅为示例。请勿直接在生产环境中运行,请根据贵公司的环境自行替换。

相关文章

相关咨询领域

小村软件有限公司承接耗时较长的运维脚本提速、包含并行处理在内的自动化设计评审,以及”并行化之后结果变得奇怪了”这类故障的调查。

参考链接

  1. 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

  2. Microsoft Learn,Start-ThreadJob。关于 Start-ThreadJob 不是在独立进程、而是在同一进程内独立的线程中执行脚本块,因此比 Start-Job 更轻量,可以用标准的作业命令(Receive-Job 等)操作,以及通过 -ThrottleLimit 控制并发数。  2 3 4 5

  3. Microsoft Learn,about_Jobs。关于后台作业会在新进程中异步执行命令,通过 Start-Job、Get-Job、Receive-Job、Wait-Job 操作作业,以及作业的结果会经过序列化后返回。  2 3 4 5 6 7

  4. Microsoft Learn,Invoke-Command。关于可以通过 -ComputerName 指定多台计算机执行相同命令,-ThrottleLimit 用于限制并发连接数且默认值为 32,以及通过 -AsJob 实现后台执行。  2 3 4 5 6

  5. 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 后再连接作为规避方案。 

  6. Microsoft Learn,about_Execution_Policies。关于执行策略是控制脚本加载条件的机制,在所有作用域均未设定时,Windows 客户端的有效策略为 Restricted、Windows Server 为 RemoteSigned,Restricted 下无法执行包括 .ps1、.psm1 在内的脚本文件,以及可以通过指定 -Scope 只应用于当前用户或进程。 

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

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 中加载模块都要花费成本,如果模块较重,即使提高并行度效果也会遇到瓶颈。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表