PowerShell多线程:深入探讨

在某个时候,大多数人都会遇到一个问题,即基本的PowerShell脚本解决问题的速度太慢。这可能涉及从网络上的许多计算机收集数据,或者一次性在Active Directory中创建大量新用户。这两种情况都是使用更多处理能力可以使您的代码运行更快的很好的例子。

默认的PowerShell会话是单线程的。它运行一个命令,当它完成时,才会转到下一个命令。这很好,因为它使一切都可以重复,并且不使用太多资源。但是,如果它执行的操作不依赖于彼此,并且您有多余的CPU资源,那么就是考虑多线程的时候了。

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

理解PowerShell多线程

多线程是一种同时运行多个命令的方法。在PowerShell通常使用单线程的情况下,有很多方法可以使用多个线程来并行化您的代码。

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

例如,如果您想在Active Directory中创建一个新用户,该怎么办?在这个例子中,没有任何需要多线程处理的东西,因为只运行了一个命令。当您想创建1000个新用户时,一切都会发生变化。

没有多线程,您将运行1000次New-ADUser命令来创建所有用户。也许创建一个新用户需要三秒钟。要创建所有1000个用户,将需要将近一个小时。与使用一个线程执行1000个命令相比,您可以使用100个线程,每个线程运行十个命令。现在,不到一分钟就可以完成,而不是大约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毫秒的差异在只运行此代码一次时不会引起注意,但如果运行数千次,它将开始累积。

time difference for running the two lines from above

使用线程安全的代码

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

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

PowerShell多线程代码不是线程安全的问题在于可能会得到不一致的结果。有时它可能运行正常,因为线程恰好正确地计时,不会引起冲突。其他时候,您将遇到冲突,由于不一致的错误,这将使问题的排除变得困难。

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

使用PSJobs进行并行执行

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

PS51> Get-Command *-Job
Get-Command *-Job output

跟踪作业的进度

所有PSJobs都处于十一个状态之一。这些状态是PowerShell用于管理作业的方式。

下面是作业最常见的几个状态的列表。

  • 已完成 – 作业已完成,可以检索输出数据或删除作业。
  • 运行中 – 作业当前正在运行,除非强行停止作业,否则无法删除。此时也无法检索输出。
  • 阻塞 – 作业仍在运行,但在继续之前,主机需要提示输入信息。
  • 失败 – 作业执行过程中发生了终止错误。

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

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

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

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

以下是用于描述作业的其他状态的示例。从 Command 列可以看出,可能导致一些作业未能完成的原因,比如尝试休眠 abc 秒导致作业失败。

All jobs

创建新作业

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

主要好处在于运行Start-Job命令所需的时间比运行我们正在使用的脚本块要少。您可以在下图中看到,与命令需要五秒钟完成相比,启动作业只需要0.15秒。

How long it takes to create a job

之所以能够在极短的时间内运行相同的代码,是因为它作为PSJob在后台运行。它只需要0.15秒来设置并开始在后台运行代码,而不是在前台运行并实际等待五秒钟。

检索作业输出

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

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

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

创建计划作业

另一种与 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、脚本块和一些其他属性,如下所示。

Registering a scheduled job

经过几次登录尝试后,您可以从下面的截图中看到已记录了这些尝试。

Example login.log text file

利用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}
Comparing speed of jobs

Runspaces:有点像作业但更快!

到目前为止,您一直在学习如何仅使用内置命令在PowerShell中使用额外线程的方法。另一种多线程脚本的选项是使用单独的运行空间。

Runspaces是线程运行PowerShell的封闭区域。虽然与PowerShell控制台一起使用的运行空间限制为单个线程,但您可以使用额外的运行空间来允许使用额外的线程。

Runspace与PS作业

虽然运行空间和PS作业有许多相似之处,但在性能上存在一些重大差异。运行空间和PS作业之间最大的区别是设置和拆除每个的时间。

在前一节的示例中,创建的PS作业大约需要150毫秒启动。这大约是最理想的情况,因为作业的脚本块几乎没有包含任何代码,并且没有将任何额外变量传递给作业。

与PS作业创建相比,运行空间是预先创建的。在添加任何代码之前,大部分启动运行空间作业的时间都已处理好。

以下是在运行空间中运行与我们用于PS作业的相同命令的示例。

Measuring speed of job creation

相比之下,以下是用于runspace版本的代码。您可以看到执行相同任务需要更多的代码。但是额外代码的好处几乎节省了3/4的时间,使得命令在36毫秒内开始运行,而不是148毫秒。

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})
$PowerShell.BeginInvoke()
Measuring speed of creating a runspace

运行Runspaces:一次漫步

使用runspaces可能一开始会让人望而生畏,因为没有了PowerShell命令的手把手指导。您将需要直接处理.NET类。在本节中,让我们分解一下在PowerShell中创建runspace所需的步骤。

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

创建Runspace

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

 $Runspace = [runspacefactory]::CreateRunspace()

现在,runspace已经创建,将其分配给一个PowerShell实例以运行PowerShell代码。为此,您将使用powershell类,并且与runspace类似,您需要将其存储到像下面所示的变量中。

 $PowerShell = [powershell]::Create()

接下来,将runspace添加到您的PowerShell实例中,打开runspace以能够运行代码,并添加您的脚本块。下面显示了一个脚本块,用于休眠五秒钟。

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

执行Runspace

到目前为止,脚本块仍然没有被运行。到目前为止,所有的工作都是为了定义runspace。要开始运行脚本块,你有两个选项。

  • Invoke()Invoke() 方法在runspace中运行脚本块,但会等待返回到控制台,直到runspace返回。这对于在放开代码之前测试代码是否正确执行非常有用。
  • BeginInvoke() – 使用 BeginInvoke() 方法是为了实际看到性能提升。这将在runspace中开始运行脚本块,并立即将您返回到控制台。

在使用 BeginInvoke() 时,将输出存储到一个变量中,因为在runspace中查看脚本块的状态时将需要这个变量,如下所示。

$Job = $PowerShell.BeginInvoke()

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

the job is completed

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

在这个例子中,不会有输出,但为了结束调用,您将使用以下命令。

$PowerShell.EndInvoke($Job)

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

$Runspace.Close()

使用运行空间池

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

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

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

运行空间池与单个运行空间的设置方式有所不同。其中一个关键差异是您定义了运行空间池可以使用的最大线程数。对于单个运行空间,它仅限于单个线程,但对于池,则指定了池可以扩展到的最大线程数。

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

运行空间池速度演示

展示一个运行空间池胜过单个运行空间的例子,也许你想要创建十个新文件。如果你使用单个运行空间来完成这个任务,你会创建第一个文件,然后转到第二个,然后转到第三个,直到全部十个文件都创建完毕。这个例子中的脚本块可能如下所示。你会在循环中向这个脚本块提供十个文件名,它们都会被创建。

$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秒。

Measuring performance of creating runspaces

现在,改用运行空间池而不是单个运行空间。以下是将要运行的代码。在使用运行空间池时,您可以在下面的代码中看到两者之间的一些区别。

$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秒。

Comparing runspaces vs. runspace pools

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

Property Runspace Runspace Pool
Wait Delay Waiting 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 Threads One Five
Runtime 50.8 Seconds 10.1 Seconds

缓慢进入Runspaces与PoshRSJob

A frequent occurrence when programming is that you will do what is more comfortable and accept the small loss in performance. This could be because it makes the code easier to write or easier to read, or it could just be your preference.

在PowerShell中也会发生同样的情况,有些人会使用PSJobs而不是runspaces,因为使用起来更方便。有一些方法可以在获得更好性能的同时,不使使用变得过于困难。

有一个广泛使用的模块叫做PoshRSJob,其中包含与普通PSJobs相匹配的模块,但又具有使用runspaces的附加优势。与必须指定所有代码以创建runspace和powershell对象不同,当运行命令时,PoshRSJob模块会处理所有这些。

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

Install-Module PoshRSJob

安装完模块后,可以看到命令与PSJob命令相同,只是有一个RS前缀。而不是Start-Job,而是Start-RSJob。而不是Get-Job,而是Get-RSJob

下面是如何在PSJob和RSJob中运行相同命令的示例。如您所见,它们具有非常相似的语法和输出,但并非完全相同。

run the same command in a PSJob and then again in an RSJob

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

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

如下所示,由于RSJobs仍在底层使用runspaces,因此存在较大的速度差异。

large speed difference since the RSJobs are still using runspaces below the covers

Foreach-Object -Parallel

PowerShell社区一直希望有一种更简单和内置的方法来快速地进行多线程处理。并行开关就是从中产生的一个解决方案。

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

虽然细节还在讨论中,但这可能是未来使用runspace的一种更简单的方法。如下所示,您可以快速循环遍历多个睡眠集合。

Measure-Command {1..10 | Foreach-Object {Start-Sleep 5}}
Measure-Command {1..10 | Foreach-Object -Parallel {Start-Sleep 5}}
Looping through and measuring commands

多线程的挑战

尽管迄今为止,多线程听起来一直很不错,但事实并非如此。多线程任何代码都会带来许多挑战。

使用变量

多线程最大且最明显的挑战之一是,您不能在不传递它们为参数的情况下共享变量。有一个例外,即“同步hashtable”,但这是另一个话题。

无论是PSJobs还是runspace,都无法访问现有变量,并且无法从控制台与不同runspace中使用的变量进行交互。

这给动态传递信息到这些任务带来了巨大的挑战。答案取决于您使用的多线程类型。

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

PSJob:

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

RSJob:

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

原生运行空间不会带来相同的便利性。相反,您必须在 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 个作业都正确完成。

Get-Content -Path .\Test.txt).Count” class=”wp-image-3241″/>
(Get-Content -Path .\Test.txt).Count

解决此问题的一些方法是记录到单独的文件中。这样可以解决文件锁定问题,但然后您需要浏览许多日志文件以弄清发生了什么。

另一种选择是允许部分输出的时间错开,并且只有在工作完成后才记录。这样可以通过您最初的会话对所有内容进行序列化,但由于您不一定知道一切发生的顺序,所以会丢失一些细节。

摘要

虽然多线程可以提供巨大的性能增益,但也可能带来一些头痛。虽然某些工作负载会大大受益,但其他一些可能根本不会。使用多线程有许多利弊,但如果使用正确,可以显著减少代码的运行时间。

进一步阅读

Source:
https://adamtheautomator.com/powershell-multithreading/