PowerShell多線程:深入挖掘

大多數人都會遇到一個問題,即基本的PowerShell腳本太慢無法解決。這可能是從您的網絡上收集大量計算機數據,或者一次在Active Directory中創建大量新用戶。這兩個例子都很好地說明了使用更多處理能力可以使代碼運行更快。讓我們來看看如何使用PowerShell多線程來解決這個問題!

默認的PowerShell會話是單線程的。它運行一條命令,完成後再運行下一條命令。這樣做很好,因為它保持了一切的可重複性,並且不占用太多資源。但是,如果它執行的操作彼此不依賴,而且您有多餘的CPU資源,那麼就該考慮使用多線程了。

在本文中,您將學習如何理解和使用各種PowerShell多線程技術,以同時處理多個數據流,但通過同一個控制台管理。

理解PowerShell多線程

多線程是一種同時運行多個命令的方法。在PowerShell通常使用單線程的情況下,有很多方法可以使用多個線程來並行執行代碼。

多線程的主要優點是減少代碼運行時間。這種時間減少是以更高的處理能力需求為代價的。在多線程時,多個操作同時進行,因此需要更多的系統資源。

例如,如果您想在Active Directory中创建一个新用户,该怎么办?在这个例子中,因为只运行一个命令,所以没有多线程的需求。但是当您想要创建1000个新用户时,情况就完全不同了。

如果没有多线程,您将运行1000次New-ADUser命令来创建所有用户。假设每创建一个新用户需要三秒钟的时间。要创建所有1000个用户,需要接近一小时的时间。但是,如果不是使用一个线程运行1000个命令,而是使用100个线程,每个线程运行十个命令,那么所需的时间就会缩短到不到一分钟!

请注意,并不会出现完美的扩展。在代码中创建和销毁项目需要一些时间。使用单个线程,PowerShell只需要运行代码即可完成任务。而使用多个线程,原始线程将用于管理其他线程。在某一点上,原始线程将无法再同时管理其他线程。

PowerShell多线程的先决条件

在本文中,您将亲自了解PowerShell多线程是如何工作的。如果您想跟随操作,请参考以下几点准备工作和有关所使用环境的详细信息。

  • Windows PowerShell 版本需為3或更高 – 除非另有說明,否則所有示範的程式碼都適用於Windows PowerShell版本3或更高。示例中將使用Windows PowerShell版本5.1。
  • 保留額外的CPU和內存 – 您至少需要一些額外的CPU和內存來使用PowerShell進行並行處理。如果沒有這些資源,您可能看不到任何性能改善。

首要目標:修復您的程式碼!

在使用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模块中内置了命令。Microsoft.PowerShell.Core模块包含在PowerShell 3版本以及后续版本中。这个模块中的命令允许您在后台运行代码的同时继续在前台运行不同的代码。您可以在下面看到所有可用的命令。

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

跟踪您的作业

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

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

  • 已完成 – 作业已经完成,可以检索输出数据或删除作业。
  • 运行中 – 作业正在运行,无法在没有强制停止作业的情况下删除。输出也无法检索。
  • 被阻止 – 作业仍在运行,但主机在继续之前需要提示信息。
  • 失敗 – 執行工作時發生了終止錯誤。

要獲取已啟動工作的狀態,請使用Get-Job命令。此命令獲取所有工作的屬性。

下面是一個工作的輸出,您可以看到狀態為已完成。下面的示例在使用Start-Job命令在工作中執行代碼Start-Sleep 5。然後使用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中使用額外的線程的方法。將腳本多線程化的另一個選項是使用單獨的runspace。

Runspaces是運行PowerShell的線程運行的封閉區域。雖然用於PowerShell控制台的runspace僅限於單個線程,但您可以使用其他runspace來允許使用其他線程。

Runspace vs PSJobs

雖然runspace和PSJob有很多相似之處,但在性能上存在一些重大差異。runspace和PSJob之間最大的區別是建立和拆除所需的時間。

在前一節的示例中,創建的PSJob花費約150毫秒的時間。這是最理想的情況,因為作業的腳本塊幾乎不包含任何代碼,並且沒有傳遞其他變量給作業。

與PSJob的創建相比,runspace是提前創建的。在添加任何代碼之前,大部分啟動runspace作業所需的時間已經處理完畢。

以下是在runspace中運行與PSJob中使用的相同命令的示例。

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 控制台中創建一個單獨的 runspace 和一個單獨的 PowerShell 實例。然後,你將將新的 runspace 指派給新的 PowerShell 實例並添加代碼到該實例中。

創建 Runspace

首先,你需要創建一個新的 runspace。你可以使用 runspacefactory 類來實現這一點。將其存儲到一個變量中,就像下面顯示的那樣,以便以後引用。

 $Runspace = [runspacefactory]::CreateRunspace()

現在,已經創建了 runspace,將其分配給 PowerShell 實例以運行 PowerShell 代碼。為此,你將使用 powershell 類,與 runspaces 類似,你需要將其存儲到變量中,就像下面顯示的那樣。

 $PowerShell = [powershell]::Create()

接下來,將 runspace 添加到 PowerShell 實例中,打開 runspace 以運行代碼並添加你的 scriptblock。下面是一個示例,其中的 scriptblock 是睡眠五秒鐘。

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

執行 Runspace

到目前为止,脚本块仍未运行。目前所做的一切只是为运行空间定义了一切。要开始运行脚本块,您有两个选项。

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

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

$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實例,使用相同的代碼塊和相同的循環。不同之處在於運行空間將自動擴展以使用這五個線程。

創建運行空間池

執行緒集區的建立與前一節中建立的執行緒相似。以下是一個示例。添加 scriptblock 和 invoke 過程與執行緒相同。如下所示,執行緒集區的最大執行緒數設為五個。

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

比較執行緒和執行緒集區的速度

為了展示執行緒和執行緒集區之間的差異,建立一個執行緒並運行先前的 Start-Sleep 命令。這一次,必須運行十次。如下代碼所示,正在建立一個會休眠五秒的執行緒。

$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}
}

請注意,由於使用的是單個執行緒,必須等待它完成後才能開始另一個 invoke。這就是為什麼在作業完成前增加了 100 毫秒的休眠時間。雖然可以減少這個時間,但你會發現花在檢查作業是否完成的時間比等待作業完成的時間更長。

從下面的示例中,你可以看到完成十組五秒的休眠大約需要 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

使用PoshRSJob逐步了解Runspaces

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的附加優點。當運行命令時,PoshRSJob模塊處理創建runspace和powershell對象的所有代碼,而無需指定所有代碼。

要安裝該模塊,在管理權限的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 参数。该过程使用 runspaces 来并行化代码,并将用于 Foreach-Object 的脚本块作为 runspaces 的脚本块。

虽然细节还在进一步完善中,但这可能是未来使用 runspaces 的一种更简单的方式。如下所示,您可以快速循环许多个休眠集合。

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

多线程的挑战

虽然多线程听起来一直都很棒,但事实并非如此。多线程任何代码都会带来许多挑战。

使用变量

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

PSJobs 和 runspaces 都无法访问现有变量,也无法从控制台与在不同 runspaces 中使用的变量进行交互。

这对于动态传递信息到这些作业构成了巨大的挑战。答案因使用的多线程类型而异。

對於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:

$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/