当前位置:网站首页 > 更多 > 玩电脑 > 正文

[玩转系统] PowerShell 多线程:深入探讨

作者:精品下载站 日期:2024-12-14 20:52:31 浏览:15 分类:玩电脑

PowerShell 多线程:深入探讨


在某些时候,大多数人都会遇到基本 PowerShell 脚本太慢而无法解决的问题。这可能是从网络上的许多计算机收集数据,或者可能同时在 Active Directory 中创建大量新用户。这些都是使用更多处理能力可以让代码运行得更快的好例子。让我们来看看如何使用 PowerShell 多线程来解决这个问题!

默认 PowerShell 会话是单线程的。它运行一个命令,完成后,它会移至下一个命令。这很好,因为它使所有内容都可重复并且不使用很多资源。但是,如果它执行的操作彼此不依赖并且您有空闲的 CPU 资源怎么办?在这种情况下,是时候开始考虑多线程了。

在本文中,您将学习如何理解和使用各种 PowerShell 多线程技术来同时处理多个数据流,但通过同一控制台进行管理。

了解 PowerShell 多线程

多线程是一种一次运行多个命令的方法。 PowerShell 通常使用单个线程,但有多种方法可以使用多个线程来并行化代码。

多线程的主要好处是减少代码的运行时间。这种时间的减少是以更高的处理能力要求为代价的。多线程时,会同时执行许多操作,因此需要更多的系统资源。

例如,如果您想在 Active Directory 中创建一个新用户该怎么办?在此示例中,没有任何多线程的内容,因为只运行一个命令。当您想要创建 1000 个新用户时,这一切都会改变。

如果没有多线程,您将运行 New-ADUser 命令 1000 次来创建所有用户。也许创建一个新用户需要三秒钟。创建全部 1000 个用户只需不到一个小时。您可以使用 100 个线程,每个线程运行 10 个命令,而不是使用一个线程执行 1000 个命令。现在,您的时间不再是大约 50 分钟,而是不到一分钟!

请注意,您不会看到完美的缩放。在代码中旋转和拆除项目的行为将需要一些时间。使用单线程,PowerShell 需要运行代码,然后就完成了。对于多个线程,用于运行控制台的原始线程将用于管理其他线程。在某个时刻,原始线程将被最大化,只需保持所有其他线程保持一致。

PowerShell 多线程的先决条件

您将在本文中亲自了解 PowerShell 多线程如何工作。如果您想继续操作,下面是您需要的一些东西以及有关正在使用的环境的一些详细信息。

  • Windows PowerShell 版本 3 或更高版本 - 除非明确说明,所有演示的代码都可以在 Windows PowerShell 版本 3 或更高版本中运行。示例将使用 Windows PowerShell 版本 5.1。
  • 备用 CPU 和内存 - 您至少需要一点额外的 CPU 和内存才能与 PowerShell 并行。如果您没有此功能,您可能看不到任何性能优势。

优先事项#1:修复您的代码!

在开始使用 PowerShell 多线程加速脚本之前,您需要完成一些准备工作。首先是优化你的代码。

虽然您可以在代码中投入更多资源以使其运行得更快,但多线程确实会带来很多额外的复杂性。如果有方法可以让您在多线程之前加速代码,那么应该首先完成它们。

识别瓶颈

并行化代码的第一步是找出导致代码变慢的原因。由于逻辑错误或额外的循环,代码可能会很慢,您可以在多线程之前进行一些修改以加快执行速度。

加速代码的常见方法的一个例子是将过滤向左移动。如果您正在与大量数据进行交互,那么您希望允许减少数据量的任何过滤都应该尽早完成。下面是获取 svchost 进程使用的 CPU 量的一些代码示例。

下面的示例读取所有正在运行的进程,然后过滤掉单个进程 (svchost)。然后选择 CPU 属性并确保该值不为空。

PS51> Get-Process | Where-Object {$_.ProcessName -eq 'svchost'} | 
	Select-Object CPU | Where-Object {$_.CPU -ne $null}

将上面的代码与下面的示例进行比较。下面是具有相同输出但排列不同的另一个代码示例。请注意,下面的代码更简单,并将所有可能的逻辑移至管道符号的左侧。这可以防止 Get-Process 返回您不关心的进程。

PS51> Get-Process -Name 'svchost' | Where-Object {$_.CPU -ne $null} | 
	Select-Object CPU

下面是运行上面两条线的时间差。虽然如果只运行此代码一次,117 毫秒的差异不会很明显,但如果运行数千次,差异就会开始增加。

[玩转系统] PowerShell 多线程:深入探讨

使用线程安全代码

接下来,确保您的代码是“线程安全的”。术语“线程安全”是指如果一个线程正在运行代码,另一个线程可以同时运行相同的代码而不会引起冲突。

例如,在两个不同的线程中写入同一文件不是线程安全的,因为它不知道首先向文件添加什么。而从文件读取的两个线程是线程安全的,因为文件没有被更改。两个线程获得相同的输出。

非线程安全的 PowerShell 多线程代码的问题是您可能会得到不一致的结果。有时它可能工作得很好,因为线程恰好在正确的时间不会引起冲突。有时,您会遇到冲突,并且由于不一致的错误而导致问题排查变得困难。

如果您一次只运行两个或三个作业,它们可能恰好排列在不同时间写入文件的位置。然后,当您将代码扩展到 20 或 30 个作业时,至少有两个作业尝试同时写入的可能性就会大大降低。

使用 PSJobs 并行执行

多线程脚本的最简单方法之一是使用 PSJobs。 PSJobs 在 Microsoft.PowerShell.Core 模块中内置了 cmdlet。 Microsoft.PowerShell.Core 模块包含在自版本 3 以来的所有版本的 PowerShell 中。此模块中的命令允许您在后台运行代码,同时继续在前台运行不同的代码。您可以在下面看到所有可用的命令。

PS51> Get-Command *-Job

[玩转系统] PowerShell 多线程:深入探讨

跟踪您的工作

所有 PS 工作均位于十一个州之一。这些状态是 PowerShell 管理作业的方式。

下面您将找到一份工作最常见的状态列表。

  • 已完成 - 作业已完成,可以检索输出数据或删除作业。
  • 正在运行 - 作业当前正在运行,如果不强制停止作业,则无法将其删除。输出也无法检索。
  • 已阻止 - 作业仍在运行,但在继续之前会提示主机输入信息。
  • 失败 - 作业执行期间发生终止错误。

要获取已启动作业的状态,请使用 Get-Job 命令。此命令获取您的作业的所有属性。

下面是作业的输出,您可以看到状态为“Completed”。下面的示例是使用“Start”在作业中执行代码“Start-Sleep 5” -Job命令。然后使用 Get-Job 命令返回该作业的状态。

PS51> Start-Job -Scriptblock {Start-Sleep 5}
PS51> Get-Job

[玩转系统] PowerShell 多线程:深入探讨

当作业状态返回已完成时,这意味着脚本块中的代码已运行并已完成执行。您还可以看到 HasMoreData 属性为 False。这意味着作业完成后没有可提供的输出。

以下是用于描述工作的其他一些状态的示例。您可以从 Command 列中看到,可能导致其中一些作业无法完成的原因(例如尝试休眠 abc 秒)导致失败的工作。

[玩转系统] PowerShell 多线程:深入探讨

创造新的就业机会

正如您在上面看到的,Start-Job 命令允许您创建一个新作业并开始执行该作业中的代码。创建作业时,您需要提供用于该作业的脚本块。然后,PSJob 创建一个具有唯一 ID 号的作业并开始运行该作业。

这里的主要好处是,运行 Start-Job 命令所需的时间比运行我们正在使用的脚本块所需的时间更少。您可以在下图中看到,该命令不需要 5 秒才能完成,而只需要 0.15 秒即可启动作业。

[玩转系统] PowerShell 多线程:深入探讨

它之所以能够在很短的时间内运行相同的代码,是因为它作为 PSJob 在后台运行。设置并开始在后台运行代码花了 0.15 秒,而不是在前台运行并实际休眠了 5 秒。

检索作业输出

有时作业内部的代码会返回输出。您可以使用 Receive-Job 命令检索该代码的输出。 Receive-Job 命令接受 PSJob 作为输入,然后将作业的输出写入控制台。作业在运行时输出的所有内容都已被存储,因此当检索作业时,它会输出当时存储的所有内容。

运行下面的代码就是一个例子。这将创建并启动一个作业,将 Hello World 写入输出。然后它检索作业的输出并将其输出到控制台。

$Job = Start-Job -ScriptBlock {Write-Output 'Hello World'}
Receive-Job $Job

[玩转系统] PowerShell 多线程:深入探讨

创建计划作业

与 PSJobs 交互的另一种方式是通过计划作业。计划作业类似于可以使用任务计划程序进行配置的 Windows 计划任务。计划作业创建了一种在计划任务中轻松计划复杂 PowerShell 脚本块的方法。使用计划作业,您可以根据触发器在后台运行 PSJob。

工作触发器

作业触发器可以是特定时间、用户登录时间、系统启动时间等。您还可以让触发器每隔一段时间重复一次。所有这些触发器都是使用 New-JobTrigger 命令定义的。该命令用于指定将运行计划作业的触发器。没有触发器的计划作业必须手动运行,但每个作业可以有多个触发器。

除了拥有触发器之外,您仍然会有一个脚本块,就像普通 PSJob 所使用的那样。如果您同时拥有触发器和脚本块,则可以使用 Register-ScheduledJob 命令来创建作业,如下一节所示。此命令用于指定计划作业的属性,例如将要运行的脚本块以及使用 New-JobTrigger 命令创建的触发器。

演示

也许您需要一些 PowerShell 代码来在每次有人登录计算机时运行。您可以为此创建一个计划作业。

为此,您首先需要使用 New-JobTrigger 定义触发器,并定义计划作业,如下所示。每次有人登录时,此计划作业都会向日志文件写入一行。

$Trigger = New-JobTrigger -AtLogon
$Script = {"User $env:USERNAME logged in at $(Get-Date -Format 'y-M-d H:mm:ss')" | Out-File -FilePath C:\Temp\Login.log -Append}

Register-ScheduledJob -Name Log_Login -ScriptBlock $Script -Trigger $Trigger

运行上述命令后,您将获得类似于创建新作业时的输出,其中将显示作业 ID、脚本块和一些其他属性,如下所示。

[玩转系统] PowerShell 多线程:深入探讨

几次登录尝试后,您可以从下面的屏幕截图中看到它已记录尝试。

[玩转系统] PowerShell 多线程:深入探讨

利用 AsJob 参数

使用作业的另一种方法是使用许多 PowerShell 命令中内置的 AsJob 参数。由于有许多不同的命令,您可以使用 Get-Command 找到所有命令,如下所示。

PS51> Get-Command -ParameterName AsJob

最流行的命令之一是 Invoke-Command。通常,当您运行此命令时,它将立即开始执行命令。虽然有些命令会立即返回,允许您继续正在进行的操作,但有些命令会等到命令完成。

使用 AsJob 参数的作用正如它听起来的那样,将执行的命令作为作业运行,而不是在控制台中同步运行。

虽然大多数时候 AsJob 可以在本地计算机上使用,但 Invoke-Command 没有在本地计算机上运行的本机选项。有一个解决方法,即使用 Localhost 作为 ComputerName 参数值。以下是此解决方法的示例。

PS51> Invoke-Command -ScriptBlock {Start-Sleep 5} -ComputerName localhost

为了显示正在运行的 AsJob 参数,下面的示例使用 Invoke-Command 休眠五秒钟,然后使用 AsJob 重复相同的命令显示执行时间的差异。

PS51> Measure-Command {Invoke-Command -ScriptBlock {Start-Sleep 5}}
PS51> Measure-Command {Invoke-Command -ScriptBlock {Start-Sleep 5} -AsJob -ComputerName localhost}

Runspaces:有点像作业,但速度更快!

到目前为止,您一直在学习仅使用内置命令通过 PowerShell 使用附加线程的方法。多线程脚本的另一个选项是使用单独的运行空间。

运行空间是运行 PowerShell 的线程在其中运行的封闭区域。虽然与 PowerShell 控制台一起使用的运行空间仅限于单个线程,但您可以使用其他运行空间来允许使用其他线程。

运行空间与 PSJobs

虽然运行空间和 PSJob 有许多相似之处,但在性能上存在一些很大的差异。运行空间和 PSjob 的最大区别在于设置和分解每一个所需的时间。

在上一节的示例中,创建的 PSjob 需要大约 150 毫秒才能启动。这是最好的情况,因为作业的脚本块根本不包含太多代码,并且没有任何其他变量传递给作业。

与 PSJob 创建相反,运行空间是提前创建的。启动运行空间作业所需的大部分时间都是在添加任何代码之前处理的。

下面是在运行空间中运行我们用于 PSjob 的相同命令的示例。

相比之下,下面是用于运行空间版本的代码。您可以看到有更多的代码来执行相同的任务。但额外代码的好处是减少了近 3/4 的时间,使命令开始运行的时间为 36 毫秒,而不是 148 毫秒。

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})
$PowerShell.BeginInvoke()

[玩转系统] PowerShell 多线程:深入探讨

运行运行空间:演练

起初,使用运行空间可能是一项艰巨的任务,因为不再需要 PowerShell 命令。您必须直接处理 .NET 类。在本节中,我们将详细介绍如何在 PowerShell 中创建运行空间。

在本演练中,您将从 PowerShell 控制台创建一个单独的运行空间和一个单独的 PowerShell 实例。然后,您将新的运行空间分配给新的 PowerShell 实例并向该实例添加代码。

创建运行空间

您需要做的第一件事是创建新的运行空间。您可以使用 runspacefactory 类来执行此操作。将其存储到变量中,如下所示,以便稍后引用。

 $Runspace = [runspacefactory]::CreateRunspace()

现在已创建运行空间,将其分配给 PowerShell 实例以运行 PowerShell 代码。为此,您将使用 powershell 类,与运行空间类似,您需要将其存储到如下所示的变量中。

 $PowerShell = [powershell]::Create()

接下来,将运行空间添加到您的 PowerShell 实例,打开运行空间以便能够运行代码并添加脚本块。如下所示,其中一个脚本块休眠五秒钟。

 $PowerShell.Runspace = $Runspace
 $Runspace.Open()
 $PowerShell.AddScript({Start-Sleep 5})

执行运行空间

到目前为止,脚本块仍然没有运行。到目前为止所做的只是定义运行空间的所有内容。要开始运行脚本块,您有两个选择。

  • Invoke() - Invoke() 方法在运行空间中运行脚本块,但它会等待返回控制台,直到运行空间返回。这对于测试很有用,可以确保您的代码在释放之前正确执行。
  • BeginInvoke() - 使用 BeginInvoke() 方法是您真正希望看到的性能提升。这将启动在运行空间中运行的脚本块并立即返回到控制台。

使用 BeginInvoke() 时,请将输出存储到变量中,因为需要查看运行空间中脚本块的状态,如下所示。

$Job = $PowerShell.BeginInvoke()

BeginInvoke() 的输出存储到变量后,您可以检查该变量以查看作业的状态,如下面的 IsCompleted 属性中所示。

您需要将输出存储在变量中的另一个原因是,与 Invoke() 方法不同,BeginInvoke() 在代码完成时不会自动返回输出。为此,您必须在完成后使用 EndInvoke() 方法。

在此示例中,不会有任何输出,但要结束调用,您可以使用以下命令。

$PowerShell.EndInvoke($Job)

一旦您在运行空间中排队的所有任务完成,您应该始终关闭运行空间。这将允许 PowerShell 的自动垃圾收集过程清理未使用的资源。以下是您将用来执行此操作的命令。

$Runspace.Close()

使用运行空间池

虽然使用运行空间确实可以提高性能,但它遇到了单线程的主要限制。这就是运行空间池在使用多线程方面的亮点。

在上一节中,您仅使用了两个运行空间。您只使用了一个用于 PowerShell 控制台本身以及您手动创建的一个。运行空间池允许您使用单个变量在后台管理多个运行空间。

虽然这种多运行空间行为可以通过多个运行空间对象来完成,但使用运行空间池可以使管理变得更加容易。

运行空间池与单个运行空间的不同之处在于它们的设置方式。主要区别之一是您定义了可用于运行空间池的最大线程数。对于单个运行空间,它仅限于单个线程,但对于池,您可以指定池可以扩展的最大线程数。

运行空间池中建议的线程数量取决于正在执行的任务数量以及运行代码的计算机。虽然在大多数情况下增加最大线程数不会对速度产生负面影响,但您可能也看不到任何好处。

运行空间池速度演示

为了展示运行空间池击败单个运行空间的示例,您可能想要创建十个新文件。如果您要使用单个运行空间来执行此任务,则需要创建第一个文件,然后移动到第二个文件,然后移动到第三个文件,依此类推,直到创建所有 10 个文件。此示例的脚本块可能如下所示。您可以在循环中为该脚本块提供十个文件名,它们都会被创建。

$Scriptblock = {
    param($Name)
    New-Item -Name $Name -ItemType File
}

在下面的示例中,定义了一个脚本块,其中包含一个简短的脚本,该脚本接受名称并使用该名称创建文件。创建的运行空间池最多包含 5 个线程。

接下来,一个循环循环十次,每次都会将迭代次数分配给$_。因此,第一次迭代时为 1,第二次迭代时为 2,依此类推。

该循环创建一个 PowerShell 对象,分配脚本块和脚本参数并启动进程。

最后,在循环结束时,它将等待所有队列任务完成。

$Scriptblock = {
    param($Name)
    New-Item -Name $Name -ItemType File
}

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$RunspacePool.Open()
$Jobs = @()

1..10 | Foreach-Object {
	$PowerShell = [powershell]::Create()
	$PowerShell.RunspacePool = $RunspacePool
	$PowerShell.AddScript($ScriptBlock).AddArgument($_)
	$Jobs += $PowerShell.BeginInvoke()
}

while ($Jobs.IsCompleted -contains $false) {
	Start-Sleep 1
}

现在,它将一次创建五个线程,而不是一次创建一个线程。如果没有运行空间池,您将必须创建和管理五个单独的运行空间和五个单独的 Powershell 实例。这种管理很快就会变得一团糟。

相反,您可以创建一个运行空间池、一个 PowerShell 实例,使用相同的代码块和相同的循环。不同之处在于,运行空间将扩展为单独使用所有五个线程。

创建运行空间池

运行空间池的创建与上一节中创建的运行空间非常相似。下面是如何执行此操作的示例。添加脚本块和调用过程与运行空间相同。如下所示,创建的运行空间池最多包含五个线程。

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$RunspacePool.Open()

比较运行空间和运行空间池的速度

要展示运行空间和运行空间池之间的差异,请创建一个运行空间并运行之前的 Start-Sleep 命令。然而,这一次必须运行 10 次。正如您在下面的代码中看到的,正在创建一个运行空间,该运行空间将休眠 5 秒。

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})

1..10 | Foreach-Object {
    $Job = $PowerShell.BeginInvoke()
    while ($Job.IsCompleted -eq $false) {Start-Sleep -Milliseconds 100}
}

请注意,由于您使用的是单个运行空间,因此您必须等到它完成才能启动另一个调用。这就是为什么在作业完成之前添加 100 毫秒的睡眠时间。虽然可以减少这种情况,但您会看到收益递减,因为您将花费更多时间检查作业是否完成,而不是等待作业完成。

从下面的示例中,您可以看到完成 10 组 5 秒睡眠大约需要 51 秒。

现在,不再使用单个运行空间,而是切换到运行空间池。下面是将要运行的代码。您可以看到,在使用运行空间池时,下面代码中两者的使用存在一些差异。

$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
$Jobs = @()

1..10 | Foreach-Object {
    $PowerShell = [powershell]::Create()
    $PowerShell.RunspacePool = $RunspacePool
    $PowerShell.AddScript({Start-Sleep 5})
    $Jobs += $PowerShell.BeginInvoke()
}
while ($Jobs.IsCompleted -contains $false) {Start-Sleep -Milliseconds 100}

如下所示,该过程仅用了 10 多秒就完成了,这比单个运行空间的 51 秒有了很大改进。

下面是这些示例中运行空间和运行空间池之间差异的细分摘要。

PropertyRunspaceRunspace PoolWait DelayWaiting for each job to finish before continuing to the next.Starting all of the jobs and then waiting until they have all finished.Amount of ThreadsOneFiveRuntime50.8 Seconds10.1 Seconds

使用 PoshRSJob 轻松进入运行空间

编程时经常发生的情况是,您会做更舒服的事情并接受性能上的微小损失。这可能是因为它使代码更容易编写或更容易阅读,也可能只是您的偏好。

同样的情况也发生在 PowerShell 中,因为易于使用,有些人会使用 PSJobs 而不是运行空间。可以采取一些措施来消除差异并获得更好的性能,而不会使使用变得更加困难。

有一个广泛使用的模块,称为 PoshRSJob,它包含与普通 PSJobs 风格匹配的模块,但具有使用运行空间的额外好处。 PoshRSJob 模块无需指定所有代码来创建运行空间和 powershell 对象,而是在您运行命令时处理所有这些操作。

要安装该模块,请在管理 PowerShell 会话中运行以下命令。

Install-Module PoshRSJob

安装模块后,您可以看到命令与带有 RS 前缀的 PSJob 命令相同。它不是 Start-Job,而是 Start-RSJob。它不是 Get-Job,而是 Get-RSJob

下面的示例说明了如何在 PSJob 中运行相同的命令,然后在 RSJob 中再次运行。正如您所看到的,它们具有非常相似的语法和输出,但并不完全相同。

[玩转系统] PowerShell 多线程:深入探讨

下面是一些代码,可用于比较 PSJob 和 RSJob 之间的速度差异。

Measure-Command {Start-Job -ScriptBlock {Start-Sleep 5}}
Measure-Command {Start-RSJob -ScriptBlock {Start-Sleep 5}}

正如您在下面看到的,由于 RSJobs 仍在使用底层的运行空间,因此存在很大的速度差异。

[玩转系统] PowerShell 多线程:深入探讨

Foreach-对象-并行

PowerShell 社区一直希望有一种更简单的内置方法来快速实现多线程进程。并行开关就是由此产生的。

截至撰写本文时,PowerShell 7 仍处于预览阶段,但他们已向 Foreach-Object 命令添加了一个 Parallel 参数。此过程使用运行空间来并行化代码,并使用用于 Foreach-Object 的脚本块作为运行空间的脚本块。

虽然细节仍在制定中,但这可能是将来使用运行空间的更简单的方法。如下所示,您可以快速循环多组睡眠。

Measure-Command {1..10 | Foreach-Object {Start-Sleep 5}}
Measure-Command {1..10 | Foreach-Object -Parallel {Start-Sleep 5}}

[玩转系统] PowerShell 多线程:深入探讨

多线程面临的挑战

虽然到目前为止,多线程听起来似乎很神奇,但事实并非如此。对任何代码进行多线程处理都会带来许多挑战。

使用变量

多线程最大、最明显的挑战之一是,如果不将变量作为参数传递,则无法共享变量。同步哈希表有一个例外,但这是改天再谈的。

PSJobs 和运行空间都无需访问现有变量即可运行,并且无法通过控制台与不同运行空间中使用的变量进行交互。

这对动态地将信息传递给这些作业提出了巨大的挑战。根据您使用的多线程类型,答案会有所不同。

对于 PoshRSJob 模块中的 Start-JobStart-RSJob,您可以使用 ArgumentList 参数来提供将传递的对象列表按照列出的顺序作为脚本块的参数。以下是用于 PSJobs 和 RSJobs 的命令示例。

PS工作:

Start-Job -Scriptblock {param ($Text) Write-Output $Text} -ArgumentList "Hello world!"

RS工作:

Start-RSJob -Scriptblock {param ($Text) Write-Output $Text} -ArgumentList "Hello world!"

Navtive 运行空间不会给您同样的轻松。相反,您必须在 PowerShell 对象上使用 AddArgument() 方法。下面是每个内容的示例。

运行空间:

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({param ($Text) Write-Output $Text})
$PowerShell.AddArgument("Hello world!")
$PowerShell.BeginInvoke()

虽然运行空间池的工作原理相同,但下面是如何向运行空间池添加参数的示例。

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$RunspacePool.Open()
$PowerShell.AddScript({param ($Text) Write-Output $Text})
$PowerShell.AddArgument("Hello world!")
$PowerShell.BeginInvoke()

记录

多线程还带来了日志记录挑战。由于每个线程彼此独立运行,因此它们无法登录到同一位置。如果您确实尝试记录一个具有多个线程的文件,则每当一个线程写入该文件时,其他线程都无法写入该文件。这可能会减慢您的代码速度,或导致其彻底失败。

作为示例,下面是一些尝试使用运行空间池中的 5 个线程向单个文件记录 100 次的代码。

$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
1..100 | Foreach-Object {
	$PowerShell = [powershell]::Create().AddScript({'Hello' | Out-File -Append -FilePath .\Test.txt})
	$PowerShell.RunspacePool = $RunspacePool
	$PowerShell.BeginInvoke()
}
$RunspacePool.Close()

从输出中您不会看到任何错误,但如果您查看文本文件的大小,您可以在下面看到并非所有 100 个作业都正确完成。

[玩转系统] PowerShell 多线程:深入探讨

解决这个问题的一些方法是将日志记录到单独的文件中。这消除了文件锁定问题,但是您有许多日志文件,您必须对其进行排序才能弄清楚发生的所有情况。

另一种选择是,您允许关闭某些输出的计时,并且仅记录作业完成后所做的操作。这允许您通过原始会话序列化所有内容,但您会丢失一些详细信息,因为您不一定知道所有内容发生的顺序。

概括

虽然多线程可以带来巨大的性能提升,但它也可能带来令人头痛的问题。虽然某些工作负载可以带来很大好处,但其他工作负载可能根本没有好处。使用多线程有很多优点和缺点,但如果使用正确,可以大大减少代码的运行时间。

进一步阅读

  • PowerShell 7 Preview 3 - Parallel 诞生时
  • 如何为脚本构建 PowerShell GUI

您需要 登录账户 后才能发表评论

取消回复欢迎 发表评论:

关灯