NOTE: I have written a better script for generic multithreading which I have covered in my post HERE. If you are looking for a script to cover your every day needs, please read that article instead as I believe it is a better script. This script, however, is easier to understand if you are looking to learn this for yourself!
This post is actually the reason that I created the blog. When I first started looking into multithreading in PowerShell V2 I really didn’t find anyone on the web that really had a good explanation or how-to. So, why would you want to multithread your scripts? Well, if you have ever tried to run a certain script against every server or even workstation in your org you know that it can take a very long time to run because it is hitting each server, one at a time, in sequence. Wouldn’t it be great if you could run your script against 20 servers at a time? As it turns out, you can, and it’s easier than you think.
All of the work comes in understanding the 4 main cmdlets surrounding multithreading in PowerShell V2: Start-Job, Wait-Job, Get-Job and Receive-Job. The basic flow states that you start all jobs, then wait for all jobs to finish, then see what jobs you have (get) and receive all the output.
|
Start-Job {Get-Process} Get-Job | Wait-Job Get-Job | Receive-Job |
So, the above basic script starts a job,then waits for all jobs to finish and then receives all the data that all jobs (only one in our case) contain. Now this basic construct is limited,because if you try to pass a variable via the code block it won’t work. That is because whatever code block you pass is a bit like opening a new PowerShell session, pasting it in, and hitting enter. Any variables that you have aren’t in that new pristine environment. The guys over at Microsoft gave us a way to pass info in though with the cmdlets argument “ArgumentList” where you can use a variable from the host session to be passed to the new session. This, however, only works when calling a script, not a code block, so we have to also use the argument “FilePath” and provide a second PowerShell script. This is actually a good thing, because it means we can multithread any script we write as long as it has a consistent output and takes something as an argument. Take the following short script:
|
### Get-OperatingSystem.ps1 ### Param($ComputerName = "LocalHost") Get-WmiObject -ComputerName $ComputerName -Class Win32_OperatingSystem |
Now, this script is perfect because it is going to return an object and take a computer name as an argument when executed. Presumably, we would normally multi-thread something that has a much longer execution time, but keep in mind that if the host is offline this script could take a long time to run.
|
### Start-MultiThread.ps1 ### $Computers = @("Computer1","Computer2","Computer3") #Start all jobs ForEach($Computer in $Computers){ Start-Job -FilePath c:ScriptGet-OperatingSystem.ps1 -ArgumentList $Computer } #Wait for all jobs Get-Job | Wait-Job #Get all job results Get-Job | Receive-Job | Out-GridView |
So there it is. It’s that easy to multithread any script that you’ve written, but this is the most basic construct. What happens if you want to control how many threads are open? How about letting the user know where the script is in execution and how things are going? Let’s start by adding a param block to the front of the script to get a bunch of information.
|
Param($ScriptFile = $(Read-Host "Enter the script file"), $ComputerList = $(Read-Host "Enter the Location of the computerlist"), $MaxThreads = 20, $SleepTimer = 500) |
Now that we have a way to get some basic setting from the user let’s read in out computer list from the file provided and kill any currently running jobs.
|
$Computers = Get-Content $ComputerList "Killing existing jobs . . ." Get-Job | Remove-Job -Force "Done." |
Now let’s get our loop control going and start making some threads
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
$i = 0 ForEach ($Computer in $Computers){ # Check to see if there are too many open threads # If there are too many threads then wait here until some close While ($(Get-Job -state running).count -ge $MaxThreads){ Write-Progress -Activity "Creating Server List" -Status "Waiting for threads to close" -CurrentOperation "$i threads created - $($(Get-Job -state running).count) threads open" -PercentComplete ($i / $Computers.count * 100) Start-Sleep -Milliseconds $SleepTimer } #"Starting job - $Computer" $i++ Start-Job -FilePath $ScriptFile -ArgumentList $Computer -Name $Computer | Out-Null Write-Progress -Activity "Creating Server List" -Status "Starting Threads" -CurrentOperation "$i threads created - $($(Get-Job -state running).count) threads open" -PercentComplete ($i / $Computers.count * 100) } |
So this section of code is pretty simple if you just break it down. First we use a while loop to hold there until the current number of running jobs is lower than the number we have declared as our maximum. The Write-Progress command is simply letting the user know what’s going on. Once we clear the while loop we are ready to add jobs to the list of running jobs. So we start another job with the Start-Job command and then write our progress out to the user. Once this block of code is done we want to wait for all the jobs to close. In my short example I used Get-Job | Wait-Job which does the job, but it hides the progress from our user, so instead I developed this little tidbit.
|
While ($(Get-Job -State Running).count -gt 0){ $ComputersStillRunning = "" ForEach ($System in $(Get-Job -state running)){$ComputersStillRunning += ", $($System.name)"} $ComputersStillRunning = $ComputersStillRunning.Substring(2) Write-Progress -Activity "Creating Server List" -Status "$($(Get-Job -State Running).count) threads remaining" -CurrentOperation "$ComputersStillRunning" -PercentComplete ($(Get-Job -State Completed).count / $(Get-Job).count * 100) Start-Sleep -Milliseconds $SleepTimer } |
So this block of code is going to read in the computer names that we are still waiting on and show them in the write-progress command. Again I’ve used a while loop which would run unchecked if not for the start-sleep that I’ve placed in there, which is like saying “Hey, only check our progress every half second or so”. If I didn’t have the start-sleep it would simply max the processor.
Once all that is done, it is simply a matter of getting the output from all our workers. You can just use Get-Job | Receive-Job to spit is all out to the console or you can push any object out to PowerShell V2’s grid view which I love oh so much.
|
Get-Job | Receive-Job | out-gridview |
So there is a script which allows you to multithread any script in your arsenal. Enjoy!
The full text for the script I use is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
Param($ScriptFile = $(Read-Host "Enter the script file"), $ComputerList = $(Read-Host "Enter the Location of the computerlist"), $MaxThreads = 20, $SleepTimer = 500, $MaxWaitAtEnd = 600, $OutputType = "Text") $Computers = Get-Content $ComputerList "Killing existing jobs . . ." Get-Job | Remove-Job -Force "Done." $i = 0 ForEach ($Computer in $Computers){ While ($(Get-Job -state running).count -ge $MaxThreads){ Write-Progress -Activity "Creating Server List" -Status "Waiting for threads to close" -CurrentOperation "$i threads created - $($(Get-Job -state running).count) threads open" -PercentComplete ($i / $Computers.count * 100) Start-Sleep -Milliseconds $SleepTimer } #"Starting job - $Computer" $i++ Start-Job -FilePath $ScriptFile -ArgumentList $Computer -Name $Computer | Out-Null Write-Progress -Activity "Creating Server List" -Status "Starting Threads" -CurrentOperation "$i threads created - $($(Get-Job -state running).count) threads open" -PercentComplete ($i / $Computers.count * 100) } $Complete = Get-date While ($(Get-Job -State Running).count -gt 0){ $ComputersStillRunning = "" ForEach ($System in $(Get-Job -state running)){$ComputersStillRunning += ", $($System.name)"} $ComputersStillRunning = $ComputersStillRunning.Substring(2) Write-Progress -Activity "Creating Server List" -Status "$($(Get-Job -State Running).count) threads remaining" -CurrentOperation "$ComputersStillRunning" -PercentComplete ($(Get-Job -State Completed).count / $(Get-Job).count * 100) If ($(New-TimeSpan $Complete $(Get-Date)).totalseconds -ge $MaxWaitAtEnd){"Killing all jobs still running . . .";Get-Job -State Running | Remove-Job -Force} Start-Sleep -Milliseconds $SleepTimer } "Reading all jobs" If ($OutputType -eq "Text"){ ForEach($Job in Get-Job){ "$($Job.Name)" "****************************************" Receive-Job $Job " " } } ElseIf($OutputType -eq "GridView"){ Get-Job | Receive-Job | Select-Object * -ExcludeProperty RunspaceId | out-gridview } |