Comparing ThreadJob to PSJobs in PowerShell 7 on Linux

There have always been a few options for running background asynchronous tasks within PowerShell. Traditionally, PowerShell (PS) Jobs was the go to method as a job could be started and then control returned to the console. PS Jobs were always heavy, in regards to resource usage, and depending on what needed to be run, this may have outweighed the benefits of running these tasks in the background.

Starting in PowerShell 6, a new type of PowerShell Job was introduced that worked with the existing PowerShell Job cmdlets, Start-ThreadJob. Unlike traditional PS Jobs which spawn a whole new host process for each running job, PS ThreadJobs run in multiple threads on the same process which vastly increases performance by lowering overhead.

Creating a Traditional PowerShell Job

PS Jobs are easy to start and consist of three commands that are in use.

  • Start-Job
  • Get-Job
  • Receive-Job

By utilizing the above three commands, we are able to start a job, see the status of a running job, and ultimately receive the results of that job. An example of this is below.

Start-Job -Name "Test One" -ScriptBlock {
	Write-Host "Starting Job, sleeping for 3 seconds"
	Start-Sleep -Seconds 3
	Write-Host "Completing Job"
}

Get-Job

Get-Job | Receive-Job
Image #1 Expand
Untitled 2020 03 29T113247.862

Creating a PowerShell Thread Job

Seems easy enough to create a regular PowerShell Job, so how about a Thread Job? Thankfully, we can use all of the same commands as before with one difference. Instead of Start-Job, we are using Start-ThreadJob.

Start-ThreadJob -Name "Test One" -ScriptBlock {
	Write-Host "Starting Job, sleeping for 3 seconds"
	Start-Sleep -Seconds 3
	Write-Host "Completing Job"
}

Get-Job

Get-Job | Receive-Job
Image #2 Expand
Untitled 2020 03 29T113305.958

As you may have been able to tell, the PSJobTypeName is different between the two. BackgroundJob vs. ThreadJob, which allows one to easily filter the Get-Job output.

Comparing Performance between a PS Job and ThreadJob

It can be difficult to compare the performance of these two jobs. Primarily because the differences are in memory usage and overall run time.

PowerShell Job Performance

As seen with the following code block, running 5 standard PowerShell Job’s that sleep for 2 seconds each takes roughly a total of 2.8 seconds, to get started.

Measure-Command {
  1..5 | ForEach-Object {
    Start-Job -ScriptBlock {
      Start-Sleep -Seconds 2
    }
  }
} | Select-Object TotalSeconds
Image #3 Expand
Untitled 2020 03 29T113325.120

In the process list below, you can see that the following pwsh.exe process spawns multiple other full host processes, one for each job. Although only four are listed here, that is primarily due to difficulty in capturing all five at once.

Image #4 Expand
Untitled 2020 03 29T113345.433

PowerShell Thread Job Performance

Running the same scriptblock but using Start-ThreadJob instead of Start-Job shows the difference in execution time, a very small 0.02 seconds versus the 2.8 above.

Measure-Command {
  1..5 | ForEach-Object {
    Start-ThreadJob -ScriptBlock {
      Start-Sleep -Seconds 2
    }
  }
} | Select-Object TotalSeconds
Image #5 Expand
Untitled 2020 03 29T113403.151

As is also apparent, there are no new PowerShell host processes spawned as a result of running additional Thread Job’s as these are all contained within the original PowerShell host process.

Image #6 Expand
Untitled 2020 03 29T113416.719

Differences Between Start-Job and Start-ThreadJob

Since Start-Job runs a full PowerShell host process, there is more customizability that can be done. Additional abilities include, but are not limited to:

  • Credential – If the sub-process needs to be run under a different user, it is possible to change the user via a PSCredential object
  • Authentication – Similar to changing the credential, different authentication methods are available such as Basic, CredSSP, Digest, Kerberos, and Negotiate
  • WorkingDirectory – As the job runs under its own process, a different working directory can be set
  • RunAs32 – If there is a need to run the process as 32-bit rather than the default of 64-bit, this can be specified here
  • PSVersion – Finally, a different PowerShell version can be specified for the job

Though Start-ThreadJob does not have those abilities, there are some additional ones that Start-ThreadJob does have that Start-Job does not. These include:

  • ThrottleLimit – Unique to this command is the ability to spawn many threads at once, but to avoid overwhelming the system, the default limit is set at 5 but can be increased

Both contain InitalizationScript which can be very useful to preface the job’s being started with a script that may load extra modules or functionality.

Unique Properties of ForEach-Object -Parallel

With PowerShell 7 came the introduction of the Parallel parameter on the ForEach-Object cmdlet. With the introduction of this capability, one may wonder what the background process is that is powers that ability. In this case, it is background ThreadJob. This means that each iteration of ForEach-Object that is passed in via the Parallel scriptblock input, will run in it’s own thread.

Measure-Command {
  1..20 | ForEach-Object -Parallel {
    Write-Host "Number: $($_)"
    Start-Sleep -Seconds 1
  } -ThrottleLimit 20
}
Image #7 Expand
Untitled 2020 03 29T113435.117

For a task that needs to sleep 1 second in between each run, and typically would take 20 seconds to run without using background tasks, the whole process takes just over a second. This is due to the ThreadJobs being used in the background and running all 20 instances at once.

Conclusion

Though there are other methods of running parallel and asynchronous tasks within PowerShell, such as RunSpaces, a ThreadJob utilizes the existing PowerShell Jobs infrastructure and makes for an easy to use and lightweight background job capability.

Not every task will make sense to run in this way, especially those that are synchronous in nature, such that need to be run one after another, but this does add flexibility. Additionally, there is still overhead that is inherent in creating each thread so it will depend on the computational task at hand if it truly makes sense to run in a background task.

Ultimately, this offers yet another flexible option for users to further utilize PowerShell Jobs to increase the capability of their scripts and functions.