PowerShell 5 introduced the capability to create custom classes. Classes are a convenient way to create an object with your own custom definition, and can include properties and methods. There could be many potential use cases for such a custom class, but here’s one that is handy: background jobs. PowerShell comes with some cmdlets for running jobs in the background, such as Start-Job, or the -AsJob parameter, but if you’ve ever worked directly with runspaces in PowerShell, you know they perform better than jobs in a multi-threading scenario.
I put together a quick custom class that can be used in a similar way to the PowerShell job cmdlets, but by creating and managing its own PowerShell instance and runspace.
First off, here’s the class (PowerShell 5 or greater required)
class BackgroundJob { # Properties hidden $PowerShell = [powershell]::Create() hidden $Handle = $null hidden $Runspace = $null $Result = $null $RunspaceID = $This.PowerShell.Runspace.ID $PSInstance = $This.PowerShell # Constructor (just code block) BackgroundJob ([scriptblock]$Code) { $This.PowerShell.AddScript($Code) } # Constructor (code block + arguments) BackgroundJob ([scriptblock]$Code,$Arguments) { $This.PowerShell.AddScript($Code) foreach ($Argument in $Arguments) { $This.PowerShell.AddArgument($Argument) } } # Constructor (code block + arguments + functions) BackgroundJob ([scriptblock]$Code,$Arguments,$Functions) { $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() $Scope = [System.Management.Automation.ScopedItemOptions]::AllScope foreach ($Function in $Functions) { $FunctionName = $Function.Split('\')[1] $FunctionDefinition = Get-Content $Function -ErrorAction Stop $SessionStateFunction = New-Object -TypeName System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $FunctionName, $FunctionDefinition, $Scope, $null $InitialSessionState.Commands.Add($SessionStateFunction) } $This.Runspace = [runspacefactory]::CreateRunspace($InitialSessionState) $This.PowerShell.Runspace = $This.Runspace $This.Runspace.Open() $This.PowerShell.AddScript($Code) foreach ($Argument in $Arguments) { $This.PowerShell.AddArgument($Argument) } } # Start Method Start() { $THis.Handle = $This.PowerShell.BeginInvoke() } # Stop Method Stop() { $This.PowerShell.Stop() } # Receive Method [object]Receive() { $This.Result = $This.PowerShell.EndInvoke($This.Handle) return $This.Result } # Remove Method Remove() { $This.PowerShell.Dispose() If ($This.Runspace) { $This.Runspace.Dispose() } } # Get Status Method [object]GetStatus() { return $This.PowerShell.InvocationStateInfo } }
To create an object with this class definition, we need to instantiate it using a constructor. There are a couple of constructor overloads to give us some different options when we create the object.
The simplest way it to just pass a block of code you want to run in the background job:
$Code = { Test-Connection 10.25.24.27 -Count 4 } $Job = [BackgroundJob]::New($Code)
To start the job, simply call the Start() method:
$Job.Start()
To check the status of the job, we can call the GetStatus() method:
$Job.GetStatus()

When the job has completed, we can receive any results outputted by the code:
$Job.Receive()

Then we close off the job. This is important to properly dispose of the PowerShell instance.
$Job.Remove()
If the job is running too long, we can close the pipeline to stop the job:
$Job.Stop()
The PowerShell instance that we created gets added to the object as the PSInstance property, so we can explore it further if we want. Here I am viewing the command that was run in the job:
$Job.PSInstance.Commands.Commands.CommandText

Another constructor allows us to pass arguments for the code when we create the object. Since the background job is running in a separate thread, it does not have access to the variables in our main thread, so we need to pass them. Remember to add a Param() block to your code when passing variables.
$ComputerName = "PC001" $Code = { Param($ComputerName) Test-Connection $ComputerName -Count 4 } $Job = [BackgroundJob]::New($Code,$ComputerName)
We can also pass multiple variables in an array like this, but in the Param() block remember to keep them in the same order you pass them.
$ComputerName = "PC001" $Count = 10 $Code = { Param($ComputerName,$Count) Test-Connection $ComputerName -Count $Count } $Job = [BackgroundJob]::New($Code,@($ComputerName,$Count))
Finally, we can also pass custom functions to the background job:
Function Ping-Computers { Param([String[]]$ComputerName, $Count) $ComputerName | foreach { Test-Connection -ComputerName $_ -Count $Count } } $ComputerName = "PC001","PC002","PC003" $Count = 10 $Code = { Param($ComputerName,$Count) Ping-Computers -ComputerName $ComputerName -Count $Count } $Job = [BackgroundJob]::New($Code,@($ComputerName,$Count),"Function:\Ping-Computers")
There are many possibilities for customising this class further, but using a custom class like this is a convenient way to spin up a job in the background, or to add some multi-threading capability to a script.
Here’s a simple example of multi-threading. First we add jobs to a hash table, then we start each job. Next we wait until each job is completed, and then return the output. The whole process takes much less time than running the commands synchronously (one after another).
# Create multiple jobs $Jobs = @{ Job1 = [BackgroundJob]::New({Test-Connection -ComputerName 10.20.17.129 -Count 5}) Job2 = [BackgroundJob]::New({Test-Connection -ComputerName 10.1.16.86 -Count 5}) Job3 = [BackgroundJob]::New({Test-Connection -ComputerName 10.21.17.216 -Count 5}) } # Start each job $Jobs.GetEnumerator() | foreach { $_.Value.Start() } # Wait for the results Do {} Until (($Jobs.GetEnumerator() | foreach {$_.Value.GetStatus().State}) -notcontains "Running") # Output the results $Jobs.GetEnumerator() | foreach {$_.Value.Receive()}

The job result will be saved to the Result property after the Receive() method is called, so they are stored in the variable and can be retrieved later if needed.
You can also create a kind of class library, which you can import each time you run a PowerShell session, so that this custom class is always available to you, which I blogged about here.
Good job, what!
Love your code!
I’ve been struggling with Runspaces for quite some time now… This really helped break it down! Importing the function was an eye opener!!!
Thanks for sharing!!!
You’re welcome, glad it helps! I highly recommend the work of Boe Prox for learning runspaces: https://blogs.technet.microsoft.com/heyscriptingguy/2015/11/26/beginning-use-of-powershell-runspaces-part-1/