powershellrunspacepowershell-sdk

The RunSpace and its closure


While working with a script that uses a RunSpace, I found that it takes up more and more system memory. As far as I understand, this is due to the fact that open RunSpace do not close when completed. They remain in memory, accumulating megabytes.

How to close the RunSpace, correctly? However, I do not know how long it will take - 1 second or 1 hour. Closes itself when completed.

As an example, I will give arbitrary scripts.

The first script is how I do the closing of the RunSpace as it is completed (and it apparently does not work).

$Array = 1..10000

$PowerShell = [PowerShell]::Create()
$RunSpace = [Runspacefactory]::CreateRunspace()
$RunSpace.Open()

$RunSpace.SessionStateProxy.SetVariable('Array', $Array)
$RunSpace.SessionStateProxy.SetVariable('PowerShell', $PowerShell)

$PowerShell.Runspace = $RunSpace

[void]$PowerShell.AddScript({

   # Fill the system memory so that it can be seen in the Task Manager.
   $Array += $Array
   $Array

   # Closing the Process, which should close the RunSpace, but this does not happen.
   $Powershell.Runspace.Dispose()
   $PowerShell.Dispose()
})

$Async = $PowerShell.BeginInvoke()

# Other jobs in the main thread...

The second script seems to be more correct, judging by the system memory. However, of course it is not applicable in life, as the Start-Sleep 10 freezes the main Process.

$Array = 1..10000

$PowerShell = [PowerShell]::Create()
$RunSpace = [Runspacefactory]::CreateRunspace()
$RunSpace.Open()

$RunSpace.SessionStateProxy.SetVariable('Array', $Array)

$PowerShell.Runspace = $RunSpace

[void]$PowerShell.AddScript({

   # Fill the system memory so that it can be seen in the Task Manager.
   $Array += $Array
   $Array
})

$Async = $PowerShell.BeginInvoke()

Start-Sleep 10

$PowerShell.EndInvoke($Async) | Out-Null
$PowerShell.RunSpace.Dispose()
$PowerShell.Dispose()

# Other jobs in the main thread...

Please write me the correct way to close the RunSpace as it is completed. Thanks you


Solution

  • Trying to dispose of a runspace from within that runspace sounds like a bad idea - in fact, if I try this in PowerShell Core 7.0-rc2, my session hangs.

    It sounds like you want to dispose of the runspace automatically, on completion of the script.

    You can set up an event handler for the System.Management.Automation.PowerShell.InvocationStateChanged event, inside of which you can check for the Completed and Failed states and close the runspace then:

    Note: Register-ObjectEvent must be used to subscribe to the event; while it is possible in principle to subscribe by passing a script block directly to .add_InvocationStateChanged() on the PowerShell instance, execution will hang when you later call .EndInvoke().

    # Note: I'm using a small array so that the output of the code better
    #       shows what's happening.
    $Array = 1..3
    
    $PowerShell = [PowerShell]::Create()
    $RunSpace = [Runspacefactory]::CreateRunspace()
    $RunSpace.Open()
    
    $RunSpace.SessionStateProxy.SetVariable('Array', $Array)
    
    $PowerShell.Runspace = $RunSpace
    
    $null = $PowerShell.AddScript( {
    
        # Fill the system memory so that it can be seen in the Task Manager.
        $Array += $Array
        $Array
    
      })
    
    # Set up an event handler for when the invocation state of the runspace changes.
    # Note: Register-ObjectEvent with -Action returns an event-job object, which
    #       we don't need in this case.
    $null = Register-ObjectEvent -InputObject $PowerShell -EventName InvocationStateChanged -Action {
      param([System.Management.Automation.PowerShell] $ps)
    
      # NOTE: Use $EventArgs.InvocationStateInfo, not $ps.InvocationStateInfo, 
      #       as the latter is updated in real-time, so for a short-running script
      #       the state may already have changed since the event fired.
      $state = $EventArgs.InvocationStateInfo.State
    
      Write-Host "Invocation state: $state"
      if ($state -in 'Completed', 'Failed') {
        # Dispose of the runspace.
        Write-Host "Disposing of runspace."
        $ps.Runspace.Dispose()
        # Note: If the runspace produces *no* output, and you don't
        #       to reuse the $PowerShell / $ps instance later, you
        #       can call $ps.Dispose() right here.
        # Speed up resource release by calling the garbage collector explicitly.
        # Note that this will pause *all* threads briefly.
        [GC]::Collect()
      }      
    
    }
    
    # Kick off execution of the script and
    # let the event handler dispose of the runspace on completion.
    Write-Host 'Starting script execution via SDK...'
    $asyncResult = $PowerShell.BeginInvoke()
    
    # Perform other processing.
    Write-Host 'Doing things...'
    1..1e5 | ForEach-Object { }
    
    # Finally, get the results from the script execution.
    # NOTE: Even though the runspace has likely already been disposed, collecting
    #       the results still works, presumably because they're
    #       stored in the $PowerShell instance, not the runspace.
    Write-Host 'Collecting script-execution results...'
    $PowerShell.EndInvoke($asyncResult)
    
    # Note that you'll have to create and assign a *new* runspace to 
    # $PowerShell.Runspace if you want to execute further code.
    # Otherwise, call $PowerShell.Dispose() to properly dispose
    # of the PowerShell instance as a whole.
    
    Write-Host 'Done.'
    

    The above should yield the following output:

    Starting script execution via SDK...
    Doing things...
    Invocation state: Running
    Invocation state: Completed
    Disposing of runspace.
    Collecting script-execution results...
    1
    2
    3
    1
    2
    3
    Done.