Jump to page sections
This article demonstrates how to use runspaces in PowerShell, providing a flexible template with commented example code that can be easily adapted to your specific needs. Years ago, a popular function called "Invoke-Async" offered similar functionality, and you may want to search for it online. However, this article focuses on customizable code examples, using a basic ping of IP addresses as a demonstration, though the code can execute any valid PowerShell commands.
For concurrency in PowerShell, many turn to jobs (Start-Job, Get-Job, Receive-Job, etc.), which are convenient but come with significant overhead, especially in PowerShell versions 2 through 5. In those versions, each job spawns a new powershell.exe process, which consumes considerable memory and CPU resources. Although Start-ThreadJob in newer versions is promising, this article revisits an "old-school" approach—runspaces—which became popular 5-7 years ago (as of October 2021).
Runspaces provide a more lightweight and efficient alternative for handling concurrency in PowerShell. In this article, I offer a template that demonstrates how to use runspaces effectively, including key aspects like returning data from runspaces via synchronized hashtables. The example is compatible with PowerShell version 2, making it a valuable resource for those working in legacy environments.
For those using PowerShell 7 or newer, modern options such as ForEach-Object -Parallel are available, but the focus here remains on runspaces. The code is intentionally written to support older versions like PowerShell 2, though it can be modified for later versions. For instance, PowerShell 3-4 introduced features like LINQ-style (v4) filtering and improved array handling, allowing you to simplify logic with expressions like $Runspaces.Handle.IsCompleted -contains $False, replacing the more verbose Select-Object -ExpandProperty approach.
The provided code is thoroughly commented to ensure clarity and ease of use. It not only demonstrates how to manage and monitor runspaces but also handles cleanup and timeout logic, offering a robust solution for concurrent execution in PowerShell.
Verbose output from an example run is included below, demonstrating the effectiveness of this approach.
Example PowerShell Runspaces Code
#requires -version 2
[CmdletBinding()]
param(
# Self-explanatory. Maximum thread count.
[Int32]
$MaximumThreadCount = 32,
# Maximum wait time in seconds.
[Int32]
$MaximumWaitTimeSeconds = 300,
[String[]]
$ComputerName = @(
'192.168.0.1',
'192.168.0.2',
'192.168.0.3',
'192.168.0.4',
'192.168.0.5'
)
)
$StartTime = Get-Date
Write-Verbose "[$($StartTime.ToString('yyyy\-MM\-dd HH\:mm\:ss'))] Script started."
<#
$ComputerName = @(
'192.168.0.1',
'192.168.0.2',
'192.168.0.3',
'192.168.0.4',
'192.168.0.5'
)
#>
# Need this for the runspaces.
$InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
# If you want to return data, use a synchronized hashtable and add it to the
# initial session state variable.
$Data = [HashTable]::Synchronized(@{})
# Ugly line separation some places, to increase readability (shorter lines,
# always helps when they fit on GitHub...).
$InitialSessionState.Variables.Add((
New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry `
-ArgumentList 'Data', $Data, ''))
# Create a runspace pool based on the initial session state variable,
# maximum thread count and $Host variable (convenient).
$RunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, $MaximumThreadCount, $InitialSessionState, $Host)
# This seems to work best. Single-threaded apartment?
$RunspacePool.ApartmentState = 'STA'
# Open/prepare the runspace pool.
$RunspacePool.Open()
# Used for the collection of the runspaces that accumulate.
$Runspaces = @()
# This is the custom script block executed for each runspace.
$ScriptBlock = {
Param([String] $ComputerName)
$Ping = Test-Connection -ComputerName $ComputerName -Quiet
# For testing a timeout... Temporary, obviously.
#Start-Sleep -Seconds 20
<#
[PSCustomObject]@{
ComputerName = $Computer
Ping = $Ping
}
#>
$Data[$ComputerName] = [PSCustomObject]@{
Ping = $Ping
TimeFinished = [DateTime]::Now
}
}
[Decimal] $ID = 0
# I use this syntax to assign results to avoid array concatenation as that creates a copy of the array
# each time, making it very expensive for large lists/arrays. No need for an ArrayList with this method.
$Runspaces = @(foreach ($Computer in $ComputerName) {
# Create a PowerShell instance and add our custom script block containing code
# to execute for each thread.
$PowerShellInstance = [System.Management.Automation.PowerShell]::Create().AddScript($ScriptBlock)
[void]$PowerShellInstance.AddParameter('ComputerName', $Computer)
$PowerShellInstance.RunspacePool = $RunspacePool
# This is "returned"/passed down the pipeline and collected outside the foreach loop
# in the variable $Runspaces, an array. To avoid array concatenation (slow when the
# array is large).
[PSCustomObject]@{
Handle = $PowerShellInstance.BeginInvoke()
PowerShell = $PowerShellInstance
ID = ++$ID
}
})
$WaitStartTime = Get-Date
while ($True) {
if (($TotalWaitedSeconds = ([DateTime]::Now - $WaitStartTime).TotalSeconds) -gt $MaximumWaitTimeSeconds) {
Write-Verbose "Timeout of $MaximumWaitTimeSeconds seconds reached. Waited $TotalWaitedSeconds seconds."
Write-Verbose "Running EndInvoke() and Dispose() on threads."
$TempStartTime = Get-Date
$Runspaces | ForEach-Object {
$_.PowerShell.EndInvoke($_.Handle)
$_.PowerShell.Dispose()
$_.PowerShell, $_.Handle = $null, $null
}
Write-Verbose "Ending and disposing of threads took $('{0:N3}' -f ((Get-Date) - $TempStartTime).TotalSeconds) seconds."
Write-Verbose "Closing runspace pool."
$TempStartTime = Get-Date
$RunspacePool.Close()
Write-Verbose "Closing the runspace pool took $('{0:N3}' -f ((Get-Date) - $TempStartTime).TotalSeconds) seconds."
break
}
if (($Runspaces | Select-Object -ExpandProperty Handle | Select-Object -ExpandProperty IsCompleted) -contains $True) {
$FinishedThreadCount = @(($FinishedRunspaces = $Runspaces | Where-Object {$True -eq $_.Handle.IsCompleted})).Count
Write-Verbose "$FinishedThreadCount threads have finished. Running EndInvoke() and Dispose() on them."
$TempStartTime = Get-Date
$FinishedRunspaces | ForEach-Object {
$_.PowerShell.EndInvoke($_.Handle)
$_.PowerShell.Dispose()
$_.PowerShell, $_.Handle = $null, $null
}
Write-Verbose "Ending and disposing of $FinishedThreadCount threads took $('{0:N3}' -f (([DateTime]::Now - $TempStartTime).TotalSeconds)) seconds."
}
if (($Runspaces | Select-Object -ExpandProperty Handle | Select-Object -ExpandProperty IsCompleted) -contains $False) {
$UnfinishedThreadCount = @($Runspaces | Where-Object {$False -eq $_.Handle.IsCompleted}).Count
Write-Verbose "Waiting for $UnfinishedThreadCount threads to finish. Waited for $('{0:N3}' -f ([DateTime]::Now - $WaitStartTime).TotalSeconds) seconds."
Start-Sleep -Milliseconds 250
}
else {
Write-Verbose "All threads finished." # Running EndInvoke() and Dispose() on threads."
## These are handled above as they finish. This turned out redundant (actually causing bugs).
<#
$TempStartTime = Get-Date
$Runspaces | ForEach-Object {
$_.PowerShell.EndInvoke($_.Handle)
$_.PowerShell.Dispose()
$_.PowerShell, $_.Handle = $null, $null
}
Write-Verbose "Ending and disposing of threads took $('{0:N3}' -f (((Get-Date) - $TempStartTime).TotalSeconds)) seconds."
#>
Write-Verbose "Closing runspace pool."
$TempStartTime = Get-Date
$RunspacePool.Close()
Write-Verbose "Closing the runspace pool took $('{0:N3}' -f (((Get-Date) - $TempStartTime).TotalSeconds)) seconds."
# Return the hashtable with results.
$Data
# Exit the infinite loop.
break
}
}
$EndTime = Get-Date
Write-Verbose "[$($EndTime.ToString('yyyy\-MM\-dd HH\:mm\:ss'))] Script finished."
Write-Verbose "Total minutes elapsed: $('{0:N5}' -f ($EndTime - $StartTime).TotalMinutes)"
Example Output
PS /home/joakim/Documents> $Results = ./Runspaces.ps1 -Verbose
VERBOSE: [2021-09-29 19:26:59] Script started.
VERBOSE: Waiting for 5 threads to finish. Waited for 0.002 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 0.256 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 0.510 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 0.765 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 1.019 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 1.274 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 1.533 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 1.787 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 2.041 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 2.295 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 2.552 seconds.
VERBOSE: Waiting for 5 threads to finish. Waited for 2.807 seconds.
VERBOSE: 3 threads have finished. Running EndInvoke() and Dispose() on them.
VERBOSE: Ending and disposing of 3 threads took 0.001 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 3.064 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 3.328 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 3.581 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 3.835 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 4.089 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 4.345 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 4.599 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 4.853 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 5.107 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 5.362 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 5.616 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 5.869 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 6.123 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 6.379 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 6.633 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 6.887 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 7.140 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 7.394 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 7.651 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 7.905 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 8.158 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 8.412 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 8.665 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 8.922 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 9.176 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 9.429 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 9.683 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 9.939 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 10.194 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 10.448 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 10.702 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 10.956 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 11.212 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 11.465 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 11.719 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 11.972 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 12.229 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 12.482 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 12.736 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 12.990 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 13.243 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 13.500 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 13.754 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 14.007 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 14.261 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 14.515 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 14.771 seconds.
VERBOSE: Waiting for 2 threads to finish. Waited for 15.025 seconds.
VERBOSE: 1 threads have finished. Running EndInvoke() and Dispose() on them.
VERBOSE: Ending and disposing of 1 threads took 0.001 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 15.282 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 15.535 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 15.791 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 16.044 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 16.298 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 16.551 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 16.805 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 17.061 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 17.315 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 17.568 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 17.822 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 18.077 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 18.330 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 18.584 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 18.837 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 19.090 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 19.347 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 19.600 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 19.853 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 20.107 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 20.361 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 20.617 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 20.871 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 21.123 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 21.377 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 21.632 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 21.886 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 22.139 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 22.393 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 22.646 seconds.
VERBOSE: Waiting for 1 threads to finish. Waited for 22.903 seconds.
VERBOSE: 1 threads have finished. Running EndInvoke() and Dispose() on them.
VERBOSE: Ending and disposing of 1 threads took 0.001 seconds.
VERBOSE: All threads finished.
VERBOSE: Closing runspace pool.
VERBOSE: Closing the runspace pool took 0.001 seconds.
VERBOSE: [2021-09-29 19:27:22] Script finished.
VERBOSE: Total minutes elapsed: 0.38736
PS /home/joakim/Documents> $Results
Name Value
---- -----
192.168.0.4 @{Ping=False; TimeFinished=9/29/2021 7:27:14 PM}
192.168.0.2 @{Ping=True; TimeFinished=9/29/2021 7:27:02 PM}
192.168.0.1 @{Ping=True; TimeFinished=9/29/2021 7:27:02 PM}
192.168.0.3 @{Ping=True; TimeFinished=9/29/2021 7:27:02 PM}
192.168.0.5 @{Ping=False; TimeFinished=9/29/2021 7:27:22 PM}
PS /home/joakim/Documents>
#>
Hope this helps you achieve your goal involving concurrent operations. Be good so you deserve to be well.
Powershell Windows Asynchronous Runspaces .NET Threads
Blog articles in alphabetical order
A
- A Look at the KLP AksjeNorden Index Mutual Fund
- A primitive hex version of the seq gnu utility, written in perl
- Accessing the Bing Search API v5 using PowerShell
- Accessing the Google Custom Search API using PowerShell
- Active directory password expiration notification
- Aksje-, fonds- og ETF-utbytterapportgenerator for Nordnet-transaksjonslogg
- Ascii art characters powershell script
- Automatically delete old IIS logs with PowerShell
C
- Calculate and enumerate subnets with PSipcalc
- Calculate the trend for financial products based on close rates
- Check for open TCP ports using PowerShell
- Check if an AD user exists with Get-ADUser
- Check when servers were last patched with Windows Update via COM or WSUS
- Compiling or packaging an executable from perl code on windows
- Convert between Windows and Unix epoch with Python and Perl
- Convert file encoding using linux and iconv
- Convert from most encodings to utf8 with powershell
- ConvertTo-Json for PowerShell version 2
- Create cryptographically secure and pseudorandom data with PowerShell
- Crypto is here - and it is not going away
- Crypto logo analysis ftw
D
G
- Get rid of Psychology in the Stock Markets
- Get Folder Size with PowerShell, Blazingly Fast
- Get Linux disk space report in PowerShell
- Get-Weather cmdlet for PowerShell, using the OpenWeatherMap API
- Get-wmiobject wrapper
- Getting computer information using powershell
- Getting computer models in a domain using Powershell
- Getting computer names from AD using Powershell
- Getting usernames from active directory with powershell
- Gnu seq on steroids with hex support and descending ranges
- Gullpriser hos Gullbanken mot spotprisen til gull
H
- Have PowerShell trigger an action when CPU or memory usage reaches certain values
- Historical view of the SnP 500 Index since 1927, when corona is rampant in mid-March 2020
- How Many Bitcoins (BTC) Are Lost
- How many people own 1 full BTC
- How to check perl module version
- How to list all AD computer object properties
- Hva det innebærer at særkravet for lån til sekundærbolig bortfaller
I
L
M
P
- Parse openssl certificate date output into .NET DateTime objects
- Parse PsLoggedOn.exe Output with PowerShell
- Parse schtasks.exe Output with PowerShell
- Perl on windows
- Port scan subnets with PSnmap for PowerShell
- PowerShell Relative Strength Index (RSI) Calculator
- PowerShell .NET regex to validate IPv6 address (RFC-compliant)
- PowerShell benchmarking module built around Measure-Command
- Powershell change the wmi timeout value
- PowerShell check if file exists
- Powershell check if folder exists
- PowerShell Cmdlet for Splitting an Array
- PowerShell Executables File System Locations
- PowerShell foreach loops and ForEach-Object
- PowerShell Get-MountPointData Cmdlet
- PowerShell Java Auto-Update Script
- Powershell multi-line comments
- Powershell prompt for password convert securestring to plain text
- Powershell psexec wrapper
- PowerShell regex to accurately match IPv4 address (0-255 only)
- Powershell regular expressions
- Powershell split operator
- Powershell vs perl at text processing
- PS2CMD - embed PowerShell code in a batch file
R
- Recursively Remove Empty Folders, using PowerShell
- Remote control mom via PowerShell and TeamViewer
- Remove empty elements from an array in PowerShell
- Remove first or last n characters from a string in PowerShell
- Rename unix utility - windows port
- Renaming files using PowerShell
- Running perl one-liners and scripts from powershell
S
- Sammenlign gullpriser og sølvpriser hos norske forhandlere av edelmetall
- Self-contained batch file with perl code
- Silver - The Underrated Investment
- Simple Morningstar Fund Report Script
- Sølv - den undervurderte investeringen
- Sort a list of computers by domain first and then name, using PowerShell
- Sort strings with numbers more humanely in PowerShell
- Sorting in ascending and descending order simultaneously in PowerShell
- Spar en slant med en optimalisert kredittkortportefølje
- Spre finansiell risiko på en skattesmart måte med flere Aksjesparekontoer
- SSH from PowerShell using the SSH.NET library
- SSH-Sessions Add-on with SCP SFTP Support
- Static Mutual Fund Portfolio the Last 2 Years Up 43 Percent
- STOXR - Currency Conversion Software - Open Exchange Rates API