PowerShell Runspaces provide the foundation for parallel processing and advanced PowerShell execution scenarios. This guide covers runspace fundamentals, parallel processing techniques, and practical implementations for performance-critical applications.
Runspace Fundamentals
Understanding Runspaces
A runspace is an operating environment for PowerShell commands, similar to a PowerShell session but more lightweight and programmatically controllable. Runspaces enable:
- Parallel Execution: Run multiple PowerShell commands simultaneously
- Resource Isolation: Separate execution environments for security and stability
- Background Processing: Non-blocking execution of long-running tasks
- Scalable Processing: Handle large datasets efficiently through parallelization
Runspace Types
# Single Runspace (default PowerShell execution)
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.Open()
# Runspace Pool for multiple parallel operations
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
# Initial Session State for custom environments
$ISS = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$ISS.Variables.Add([System.Management.Automation.Runspaces.SessionStateVariableEntry]::new("MyVar", "MyValue", "Custom variable"))
$CustomRunspace = [runspacefactory]::CreateRunspace($ISS)
Parallel Processing Implementation
Basic Parallel Processing Framework
function Invoke-ParallelProcessing
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object[]]$InputObject,
[Parameter(Mandatory = $true)]
[scriptblock]$ScriptBlock,
[Parameter()]
[int]$ThrottleLimit = 5,
[Parameter()]
[hashtable]$Parameters = @{},
[Parameter()]
[string[]]$ImportModules = @(),
[Parameter()]
[hashtable]$ImportVariables = @{}
)
begin
{
# Create Initial Session State
$ISS = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
# Import specified modules
foreach ($Module in $ImportModules)
{
$ISS.ImportPSModule($Module)
}
# Add variables to session state
foreach ($VarName in $ImportVariables.Keys)
{
$Variable = [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new(
$VarName,
$ImportVariables[$VarName],
"Imported variable"
)
$ISS.Variables.Add($Variable)
}
# Create Runspace Pool
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $ThrottleLimit, $ISS, $Host)
$RunspacePool.Open()
$Jobs = [System.Collections.Generic.List[object]]@()
$Results = [System.Collections.Generic.List[object]]@()
}
process
{
foreach ($Item in $InputObject)
{
# Create PowerShell instance
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
# Add script block and parameters
[void]$PowerShell.AddScript($ScriptBlock)
[void]$PowerShell.AddArgument($Item)
# Add additional parameters
foreach ($ParamName in $Parameters.Keys)
{
[void]$PowerShell.AddParameter($ParamName, $Parameters[$ParamName])
}
# Start async execution
$AsyncResult = $PowerShell.BeginInvoke()
$Jobs.Add([PSCustomObject]@{
PowerShell = $PowerShell
AsyncResult = $AsyncResult
Input = $Item
StartTime = Get-Date
})
}
}
end
{
try
{
Write-Verbose "Processing $($Jobs.Count) jobs with $ThrottleLimit concurrent runspaces"
# Wait for all jobs to complete
while ($Jobs.Count -gt 0)
{
$CompletedJobs = $Jobs | Where-Object { $_.AsyncResult.IsCompleted }
foreach ($Job in $CompletedJobs)
{
try
{
# Get results
$Result = $Job.PowerShell.EndInvoke($Job.AsyncResult)
# Check for errors
if ($Job.PowerShell.Streams.Error.Count -gt 0)
{
$ErrorInfo = [PSCustomObject]@{
Input = $Job.Input
Result = $null
Error = $Job.PowerShell.Streams.Error | ForEach-Object { $_.ToString() }
Duration = (Get-Date) - $Job.StartTime
Status = "Error"
}
$Results.Add($ErrorInfo)
}
else
{
$SuccessInfo = [PSCustomObject]@{
Input = $Job.Input
Result = $Result
Error = $null
Duration = (Get-Date) - $Job.StartTime
Status = "Success"
}
$Results.Add($SuccessInfo)
}
}
catch
{
$ExceptionInfo = [PSCustomObject]@{
Input = $Job.Input
Result = $null
Error = $_.Exception.Message
Duration = (Get-Date) - $Job.StartTime
Status = "Exception"
}
$Results.Add($ExceptionInfo)
}
finally
{
# Cleanup
$Job.PowerShell.Dispose()
$Jobs.Remove($Job)
}
}
if ($Jobs.Count -gt 0)
{
Start-Sleep -Milliseconds 100
}
}
}
finally
{
# Cleanup runspace pool
$RunspacePool.Close()
$RunspacePool.Dispose()
}
Write-Verbose "Parallel processing completed. $($Results.Count) results returned."
return $Results
}
}
Advanced Network Scanner Example
function Invoke-ParallelNetworkScan
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]$ComputerName,
[Parameter()]
[int[]]$Port = @(22, 80, 135, 139, 443, 445, 3389, 5985, 5986),
[Parameter()]
[int]$TimeoutMs = 1000,
[Parameter()]
[int]$ThrottleLimit = 20
)
$ScanScript = {
param($Computer, $Ports, $Timeout)
$Results = [System.Collections.Generic.List[object]]@()
foreach ($PortNumber in $Ports)
{
try
{
$TcpClient = New-Object System.Net.Sockets.TcpClient
$AsyncResult = $TcpClient.BeginConnect($Computer, $PortNumber, $null, $null)
$Connected = $AsyncResult.AsyncWaitHandle.WaitOne($Timeout, $false)
if ($Connected)
{
try
{
$TcpClient.EndConnect($AsyncResult)
$Status = "Open"
}
catch
{
$Status = "Filtered"
}
}
else
{
$Status = "Closed"
}
$TcpClient.Close()
$Results.Add([PSCustomObject]@{
ComputerName = $Computer
Port = $PortNumber
Status = $Status
ResponseTime = if ($Status -eq "Open") { $Timeout } else { $null }
})
}
catch
{
$Results.Add([PSCustomObject]@{
ComputerName = $Computer
Port = $PortNumber
Status = "Error"
ResponseTime = $null
Error = $_.Exception.Message
})
}
}
return $Results
}
# Create input objects for parallel processing
$ScanJobs = foreach ($Computer in $ComputerName)
{
[PSCustomObject]@{
Computer = $Computer
Ports = $Port
Timeout = $TimeoutMs
}
}
Write-Host "Starting parallel network scan of $($ComputerName.Count) hosts..." -ForegroundColor Green
$ScanResults = $ScanJobs | Invoke-ParallelProcessing -ScriptBlock $ScanScript -ThrottleLimit $ThrottleLimit
# Process results
$AllScanResults = $ScanResults | Where-Object Status -eq "Success" | ForEach-Object { $_.Result }
$FailedScans = $ScanResults | Where-Object Status -ne "Success"
# Generate summary
$OpenPorts = $AllScanResults | Where-Object Status -eq "Open"
$Summary = @{
TotalHosts = $ComputerName.Count
HostsScanned = ($AllScanResults | Group-Object ComputerName).Count
TotalPorts = $AllScanResults.Count
OpenPorts = $OpenPorts.Count
ClosedPorts = ($AllScanResults | Where-Object Status -eq "Closed").Count
FilteredPorts = ($AllScanResults | Where-Object Status -eq "Filtered").Count
Errors = $FailedScans.Count
}
Write-Host "`nScan Summary:" -ForegroundColor Cyan
Write-Host "Hosts Scanned: $($Summary.HostsScanned)/$($Summary.TotalHosts)" -ForegroundColor White
Write-Host "Open Ports: $($Summary.OpenPorts)" -ForegroundColor Green
Write-Host "Closed Ports: $($Summary.ClosedPorts)" -ForegroundColor Red
Write-Host "Filtered Ports: $($Summary.FilteredPorts)" -ForegroundColor Yellow
Write-Host "Scan Errors: $($Summary.Errors)" -ForegroundColor Magenta
return [PSCustomObject]@{
ScanResults = $AllScanResults
Summary = $Summary
Errors = $FailedScans
}
}
Advanced Runspace Patterns
Background Job Manager
class RunspaceJobManager
{
[System.Collections.Generic.Dictionary[string, object]]$Jobs
[System.Management.Automation.Runspaces.RunspacePool]$RunspacePool
[int]$MaxConcurrentJobs
RunspaceJobManager([int]$MaxJobs)
{
$this.MaxConcurrentJobs = $MaxJobs
$this.Jobs = [System.Collections.Generic.Dictionary[string, object]]::new()
$this.InitializeRunspacePool()
}
[void]InitializeRunspacePool()
{
$ISS = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$this.RunspacePool = [runspacefactory]::CreateRunspacePool(1, $this.MaxConcurrentJobs, $ISS, $Host)
$this.RunspacePool.Open()
}
[string]StartJob([scriptblock]$ScriptBlock, [hashtable]$Parameters, [string]$JobName)
{
if ([string]::IsNullOrEmpty($JobName))
{
$JobName = "Job_$([guid]::NewGuid().ToString().Substring(0,8))"
}
if ($this.Jobs.ContainsKey($JobName))
{
throw "Job with name '$JobName' already exists"
}
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $this.RunspacePool
[void]$PowerShell.AddScript($ScriptBlock)
foreach ($ParamName in $Parameters.Keys)
{
[void]$PowerShell.AddParameter($ParamName, $Parameters[$ParamName])
}
$AsyncResult = $PowerShell.BeginInvoke()
$this.Jobs[$JobName] = [PSCustomObject]@{
Name = $JobName
PowerShell = $PowerShell
AsyncResult = $AsyncResult
StartTime = Get-Date
Status = "Running"
Result = $null
Error = $null
}
return $JobName
}
[object]GetJobResult([string]$JobName)
{
if (-not $this.Jobs.ContainsKey($JobName))
{
throw "Job '$JobName' not found"
}
$Job = $this.Jobs[$JobName]
if ($Job.Status -eq "Running")
{
if ($Job.AsyncResult.IsCompleted)
{
try
{
$Job.Result = $Job.PowerShell.EndInvoke($Job.AsyncResult)
$Job.Status = "Completed"
if ($Job.PowerShell.Streams.Error.Count -gt 0)
{
$Job.Error = $Job.PowerShell.Streams.Error | ForEach-Object { $_.ToString() }
$Job.Status = "CompletedWithErrors"
}
}
catch
{
$Job.Error = $_.Exception.Message
$Job.Status = "Failed"
}
finally
{
$Job.EndTime = Get-Date
$Job.Duration = $Job.EndTime - $Job.StartTime
}
}
}
return $Job
}
[object[]]GetAllJobs()
{
$JobList = [System.Collections.Generic.List[object]]@()
foreach ($JobName in $this.Jobs.Keys)
{
$JobList.Add($this.GetJobResult($JobName))
}
return $JobList.ToArray()
}
[object[]]GetCompletedJobs()
{
return $this.GetAllJobs() | Where-Object { $_.Status -in @("Completed", "CompletedWithErrors", "Failed") }
}
[object[]]GetRunningJobs()
{
return $this.GetAllJobs() | Where-Object { $_.Status -eq "Running" }
}
[void]WaitForJob([string]$JobName, [int]$TimeoutSeconds = 300)
{
$Job = $this.Jobs[$JobName]
$TimeoutTime = (Get-Date).AddSeconds($TimeoutSeconds)
while ($Job.AsyncResult.IsCompleted -eq $false -and (Get-Date) -lt $TimeoutTime)
{
Start-Sleep -Milliseconds 500
}
if ((Get-Date) -ge $TimeoutTime)
{
throw "Job '$JobName' timed out after $TimeoutSeconds seconds"
}
}
[void]WaitForAllJobs([int]$TimeoutSeconds = 300)
{
$TimeoutTime = (Get-Date).AddSeconds($TimeoutSeconds)
do
{
$RunningJobs = $this.GetRunningJobs()
if ($RunningJobs.Count -eq 0) { break }
Start-Sleep -Milliseconds 1000
} while ((Get-Date) -lt $TimeoutTime)
$StillRunning = $this.GetRunningJobs()
if ($StillRunning.Count -gt 0)
{
throw "Timeout waiting for $($StillRunning.Count) jobs to complete"
}
}
[void]RemoveJob([string]$JobName)
{
if ($this.Jobs.ContainsKey($JobName))
{
$Job = $this.Jobs[$JobName]
if ($Job.Status -eq "Running")
{
$Job.PowerShell.Stop()
}
$Job.PowerShell.Dispose()
$this.Jobs.Remove($JobName)
}
}
[void]Dispose()
{
# Clean up all jobs
foreach ($JobName in $this.Jobs.Keys.Clone())
{
$this.RemoveJob($JobName)
}
# Close and dispose runspace pool
if ($this.RunspacePool)
{
$this.RunspacePool.Close()
$this.RunspacePool.Dispose()
}
}
}
# Usage Example
function Test-RunspaceJobManager
{
$JobManager = [RunspaceJobManager]::new(5)
try
{
# Start multiple background jobs
$Job1 = $JobManager.StartJob({
param($Seconds)
Start-Sleep $Seconds
return "Job completed after $Seconds seconds"
}, @{ Seconds = 5 }, "LongRunningJob")
$Job2 = $JobManager.StartJob({
Get-Process | Where-Object CPU -gt 100 | Select-Object Name, CPU
}, @{}, "ProcessJob")
$Job3 = $JobManager.StartJob({
param($Path)
Get-ChildItem $Path -Recurse | Measure-Object Length -Sum
}, @{ Path = "C:\Windows" }, "FileSystemJob")
# Monitor jobs
Write-Host "Started $($JobManager.GetRunningJobs().Count) jobs" -ForegroundColor Green
# Wait for specific job
$JobManager.WaitForJob("ProcessJob", 30)
$ProcessResult = $JobManager.GetJobResult("ProcessJob")
Write-Host "Process job completed: $($ProcessResult.Result.Count) high-CPU processes found" -ForegroundColor Cyan
# Wait for all jobs
Write-Host "Waiting for all jobs to complete..." -ForegroundColor Yellow
$JobManager.WaitForAllJobs(60)
# Get all results
$AllJobs = $JobManager.GetCompletedJobs()
foreach ($Job in $AllJobs)
{
Write-Host "Job '$($Job.Name)' - Status: $($Job.Status) - Duration: $($Job.Duration.TotalSeconds)s" -ForegroundColor White
}
}
finally
{
$JobManager.Dispose()
}
}
Performance Optimization
Memory-Efficient Processing
function Invoke-OptimizedParallelProcessing
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[object[]]$InputData,
[Parameter(Mandatory = $true)]
[scriptblock]$ProcessingScript,
[Parameter()]
[int]$BatchSize = 100,
[Parameter()]
[int]$MaxConcurrentBatches = 4,
[Parameter()]
[string]$TempPath = $env:TEMP
)
try
{
Write-Host "Processing $($InputData.Count) items in batches of $BatchSize..." -ForegroundColor Green
# Split data into batches
$Batches = [System.Collections.Generic.List[object[]]]@()
for ($i = 0; $i -lt $InputData.Count; $i += $BatchSize)
{
$BatchEnd = [Math]::Min($i + $BatchSize - 1, $InputData.Count - 1)
$Batch = $InputData[$i..$BatchEnd]
$Batches.Add($Batch)
}
Write-Host "Created $($Batches.Count) batches for processing" -ForegroundColor Cyan
# Process batches in parallel
$BatchResults = [System.Collections.Generic.List[object]]@()
$JobManager = [RunspaceJobManager]::new($MaxConcurrentBatches)
try
{
# Start batch processing jobs
for ($BatchIndex = 0; $BatchIndex -lt $Batches.Count; $BatchIndex++)
{
$JobName = "Batch_$BatchIndex"
$BatchScript = {
param($BatchData, $Script, $BatchId)
$Results = [System.Collections.Generic.List[object]]@()
for ($ItemIndex = 0; $ItemIndex -lt $BatchData.Count; $ItemIndex++)
{
try
{
$Item = $BatchData[$ItemIndex]
$Result = & $Script $Item
$Results.Add([PSCustomObject]@{
BatchId = $BatchId
ItemIndex = $ItemIndex
Input = $Item
Result = $Result
Status = "Success"
Error = $null
})
}
catch
{
$Results.Add([PSCustomObject]@{
BatchId = $BatchId
ItemIndex = $ItemIndex
Input = $Item
Result = $null
Status = "Error"
Error = $_.Exception.Message
})
}
# Memory cleanup
if ($ItemIndex % 10 -eq 0)
{
[System.GC]::Collect()
}
}
return $Results.ToArray()
}
$Parameters = @{
BatchData = $Batches[$BatchIndex]
Script = $ProcessingScript
BatchId = $BatchIndex
}
$JobManager.StartJob($BatchScript, $Parameters, $JobName)
}
# Wait for batches to complete and collect results
$CompletedBatches = 0
while ($CompletedBatches -lt $Batches.Count)
{
$CompletedJobs = $JobManager.GetCompletedJobs()
foreach ($Job in $CompletedJobs)
{
if ($Job.Name -match "Batch_(\d+)" -and $Job.Status -eq "Completed")
{
$BatchResults.AddRange($Job.Result)
$JobManager.RemoveJob($Job.Name)
$CompletedBatches++
Write-Progress -Activity "Processing Batches" -Status "Completed batch $CompletedBatches of $($Batches.Count)" -PercentComplete (($CompletedBatches / $Batches.Count) * 100)
}
}
Start-Sleep -Milliseconds 500
}
}
finally
{
$JobManager.Dispose()
}
Write-Progress -Activity "Processing Batches" -Completed
# Generate processing summary
$Summary = @{
TotalItems = $InputData.Count
ProcessedItems = $BatchResults.Count
SuccessfulItems = ($BatchResults | Where-Object Status -eq "Success").Count
ErrorItems = ($BatchResults | Where-Object Status -eq "Error").Count
Batches = $Batches.Count
BatchSize = $BatchSize
SuccessRate = [math]::Round((($BatchResults | Where-Object Status -eq "Success").Count / $BatchResults.Count) * 100, 2)
}
Write-Host "`nProcessing Summary:" -ForegroundColor Cyan
Write-Host "Total Items: $($Summary.TotalItems)" -ForegroundColor White
Write-Host "Successful: $($Summary.SuccessfulItems)" -ForegroundColor Green
Write-Host "Errors: $($Summary.ErrorItems)" -ForegroundColor Red
Write-Host "Success Rate: $($Summary.SuccessRate)%" -ForegroundColor Yellow
return [PSCustomObject]@{
Results = $BatchResults
Summary = $Summary
}
}
catch
{
Write-Error "Optimized parallel processing failed: $($_.Exception.Message)"
throw
}
}
# Usage Examples
Write-Host @"
POWERSHELL RUNSPACES EXAMPLES
1. Basic Parallel Processing:
$Results = 1..100 | Invoke-ParallelProcessing -ScriptBlock { param($num) $num * $num } -ThrottleLimit 10
2. Network Scanning:
$ScanResults = Invoke-ParallelNetworkScan -ComputerName @("server1", "server2", "server3") -Port @(80, 443, 3389)
3. Background Job Management:
$JobManager = [RunspaceJobManager]::new(5)
$JobId = $JobManager.StartJob({ Get-Process }, @{}, "ProcessJob")
4. Optimized Batch Processing:
$Data = 1..10000
$Results = Invoke-OptimizedParallelProcessing -InputData $Data -ProcessingScript { param($x) Start-Sleep 1; $x * 2 }
These examples demonstrate PowerShell runspace capabilities for:
- High-performance parallel processing
- Background job management
- Memory-efficient batch processing
- Network operations at scale
- Advanced threading patterns
Runspaces provide significant performance improvements for:
- Large dataset processing
- Network operations
- File system operations
- API calls and web requests
- CPU-intensive calculations
"@ -ForegroundColor Green