about_Thread_Jobs
简短说明
提供有关基于 PowerShell 线程的作业的信息。 线程作业是一种后台作业,在当前会话进程中的单独线程中运行命令或表达式。
长说明
PowerShell 通过作业并发运行命令和脚本。 PowerShell 提供三种作业类型来支持并发。
RemoteJob
- 命令和脚本在远程会话中运行。 有关信息,请参阅 about_Remote_Jobs。BackgroundJob
- 命令和脚本在本地计算机上的单独进程中运行。 有关详细信息,请参阅 about_Jobs。PSTaskJob
或ThreadJob
- 命令和脚本在本地计算机上的同一进程中的单独线程中运行。
基于线程的作业不像远程和后台作业那样可靠,因为它们在不同的线程上的同一进程中运行。 如果一个作业出现导致进程崩溃的严重错误,则进程中的所有其他作业都会终止。
但是,基于线程的作业需要的开销更少。 它们不使用远程处理层或序列化。 结果对象作为对当前会话中的活动对象的引用返回。 如果没有这种开销,基于线程的作业的运行速度更快,使用的资源比其他作业类型少。
重要
创建作业的父会话还会监视作业状态并收集管道数据。 作业达到完成状态后,父进程将终止作业子进程。 如果终止父会话,则所有正在运行的子作业将连同其子进程一起终止。
有两种方法可以解决此问题:
- 使用
Invoke-Command
创建在断开连接的会话中运行的作业。 有关详细信息,请参阅 about_Remote_Jobs。 - 使用
Start-Process
创建新进程,而不是作业。 有关详细信息,请参阅 Start-Process。
如何启动和管理基于线程的作业
有两种方法可以启动基于线程的作业:
Start-ThreadJob
- 来自 ThreadJob 模块ForEach-Object -Parallel -AsJob
- PowerShell 7.0 中添加了并行功能
使用 about_Jobs 中所述的相同作业 cmdlet 来管理基于线程的作业。
使用 Start-ThreadJob
ThreadJob 模块首次随 PowerShell 6 一起提供。 也可以从 Windows PowerShell 5.1 的PowerShell 库安装它。
若要在本地计算机上启动线程作业,请将 Start-ThreadJob
cmdlet 与命令或脚本一起使用,该命令或脚本括在大括号 ({ }
) 。
以下示例启动一个线程作业,该作业在本地计算机上运行 Get-Process
命令。
Start-ThreadJob -ScriptBlock { Get-Process }
命令 Start-ThreadJob
返回表示 ThreadJob
正在运行的作业的 对象。 作业对象包含有关作业的有用信息,包括其当前运行状态。 生成结果时,它会收集作业的结果。
使用 ForEach-Object -Parallel -AsJob
PowerShell 7.0 向 ForEach-Object
cmdlet 添加了一个新参数集。 新参数允许你以 PowerShell 作业的形式在并行线程中运行脚本块。
可以通过管道将数据传递给 ForEach-Object -Parallel
。 数据将传递到并行运行的脚本块。 参数 -AsJob
为每个并行线程创建作业对象。
以下命令启动一个作业,该作业包含传递给命令的每个输入值的子作业。 每个子作业使用管道输入值作为 参数运行 Write-Output
命令。
1..5 | ForEach-Object -Parallel { Write-Output $_ } -AsJob
命令 ForEach-Object -Parallel
返回一个 PSTaskJob
对象,该对象包含每个管道输入值的子作业。 作业对象包含有关子作业运行状态的有用信息。 它在生成结果时收集子作业的结果。
如何等待作业完成并检索作业结果
可以使用 PowerShell 作业 cmdlet(如 Wait-Job
和 Receive-Job
)等待作业完成,然后返回作业生成的所有结果。
以下命令启动运行命令的 Get-Process
线程作业,然后等待命令完成,最后返回该命令生成的所有数据结果。
Start-ThreadJob -ScriptBlock { Get-Process } | Wait-Job | Receive-Job
以下命令启动一个作业,该作业为每个经过管道传递的输入运行 Write-Output
命令,然后等待所有子作业完成,最后返回子作业生成的所有数据结果。
1..5 | ForEach-Object -Parallel { Write-Output $_ } -AsJob | Wait-Job | Receive-Job
cmdlet Receive-Job
返回子作业的结果。
1
3
2
4
5
由于每个子作业并行运行,因此无法保证生成结果的顺序。
线程作业性能
线程作业比其他类型的作业更快、更轻。 但是,与工作相比,它们的开销可能很大。
PowerShell 在会话中运行命令和脚本。 会话中一次只能运行一个命令或脚本。 因此,在运行多个作业时,每个作业在单独的会话中运行。 每个会话都会造成开销。
当线程作业执行的工作大于用于运行作业的会话开销时,线程作业可提供最佳性能。 有两种情况符合此条件。
工作是计算密集型 - 在多个线程作业上运行脚本可以利用多个处理器核心并更快地完成。
工作包括大量等待 - 一个花费时间等待 I/O 或远程调用结果的脚本。 并行运行通常比按顺序运行更快。
(Measure-Command {
1..1000 | ForEach { Start-ThreadJob { Write-Output "Hello $using:_" } } | Receive-Job -Wait
}).TotalMilliseconds
36860.8226
(Measure-Command {
1..1000 | ForEach-Object { "Hello: $_" }
}).TotalMilliseconds
7.1975
上面的第一个示例演示了一个 foreach 循环,该循环创建 1000 个线程作业以执行简单的字符串写入。 由于作业开销,它需要 36 秒以上才能完成。
第二个 ForEach
示例运行 cmdlet 以执行相同的 1000 个操作。
这一次, ForEach-Object
在单个线程上按顺序运行,没有任何作业开销。 它只需 7 毫秒就能完成。
在以下示例中,最多为 10 个单独的系统日志收集了 5000 个条目。 由于脚本涉及读取大量日志,因此并行执行操作是有意义的。
$logNames.count
10
Measure-Command {
$logs = $logNames | ForEach-Object {
Get-WinEvent -LogName $_ -MaxEvents 5000 2>$null
}
}
TotalMilliseconds : 252398.4321 (4 minutes 12 seconds)
$logs.Count
50000
脚本在并行运行作业的时间的一半内完成。
Measure-Command {
$logs = $logNames | ForEach {
Start-ThreadJob {
Get-WinEvent -LogName $using:_ -MaxEvents 5000 2>$null
} -ThrottleLimit 10
} | Wait-Job | Receive-Job
}
TotalMilliseconds : 115994.3 (1 minute 56 seconds)
$logs.Count
50000
线程作业和变量
可通过多种方式将值传递到基于线程的作业中。
Start-ThreadJob
可以接受通过管道传递到 cmdlet、通过$using
关键字 (keyword) 传递到脚本块或通过 ArgumentList 参数传入的变量。
$msg = "Hello"
$msg | Start-ThreadJob { $input | Write-Output } | Wait-Job | Receive-Job
Start-ThreadJob { Write-Output $using:msg } | Wait-Job | Receive-Job
Start-ThreadJob { param ([string] $message) Write-Output $message } -ArgumentList @($msg) |
Wait-Job | Receive-Job
ForEach-Object -Parallel
接受通过管道传入的变量,以及通过 关键字 (keyword) 直接传递到脚本块的$using
变量。
$msg = "Hello"
$msg | ForEach-Object -Parallel { Write-Output $_ } -AsJob | Wait-Job | Receive-Job
1..1 | ForEach-Object -Parallel { Write-Output $using:msg } -AsJob | Wait-Job | Receive-Job
由于线程作业在同一进程中运行,因此必须仔细处理传递到作业的任何变量引用类型。 如果它不是线程安全对象,则永远不要将其分配给 ,并且永远不要对它调用方法和属性。
以下示例将线程安全的 .NET ConcurrentDictionary
对象传递给所有子作业,以收集唯一命名的进程对象。 由于它是线程安全对象,因此当作业在进程中并发运行时,可以安全地使用它。
$threadSafeDictionary = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
$jobs = Get-Process | ForEach {
Start-ThreadJob {
$proc = $using:_
$dict = $using:threadSafeDictionary
$dict.TryAdd($proc.ProcessName, $proc)
}
}
$jobs | Wait-Job | Receive-Job
$threadSafeDictionary.Count
96
$threadSafeDictionary["pwsh"]
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
112 108.25 124.43 69.75 16272 1 pwsh