about_Thread_Jobs

简短说明

提供关于 PowerShell 基于线程的作业的信息。 线程作业是一种后台作业,可在当前会话进程中的单独线程中运行命令或表达式。

长说明

PowerShell 通过作业同时运行命令和脚本。 PowerShell 提供了三种类型的作业来支持并发。

  • RemoteJob - 命令和脚本在远程会话中运行。 有关信息,请参阅 about_Remote_Jobs
  • BackgroundJob - 命令和脚本在本地计算机上的单独进程中运行。 有关详细信息,请参阅 about_Jobs
  • PSTaskJobThreadJob - 命令和脚本在本地计算机上同一进程中的单独线程中运行。

基于线程的作业不像远程作业和后台作业那样可靠,因为它们在不同线程的同一进程中运行。 如果一个作业发生严重错误,导致进程崩溃,则进程中的所有其他作业都会终止。

但是,基于线程的作业开销更少。 它们不使用远程层或序列化。 结果对象作为对当前会话中的实时对象的引用返回。 基于线程的作业没有这项开销,因此运行速度更快,使用的资源比其他作业类型更少。

重要

创建作业的父会话还会监视作业状态并收集管道数据。 作业子进程在作业达到完成状态后由父进程终止。 如果父会话终止,则所有正在运行的子作业将连同其子进程一起终止。

可通过两种方法应对此情况:

  1. 使用 Invoke-Command 创建在断开连接的会话中运行的作业。 有关详细信息,请参阅 about_Remote_Jobs
  2. 使用 Start-Process 创建新进程而不是作业。 有关详细信息,请参阅 Start-Process

如何启动和管理基于线程的作业

可通过以下两种方式启动基于线程的作业:

  • Start-ThreadJob - 从 ThreadJob 模块
  • ForEach-Object -Parallel -AsJob - PowerShell 7.0 中新增了并行功能

使用 about_Jobs 中介绍的相同的 Job 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-JobReceive-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

Receive-Job cmdlet 将返回子作业的结果。

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 关键字传入脚本块或通过 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 接受通过管道传递的变量,并通过 $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

另请参阅