I’ve had requests for a while to talk about sharing data between runspaces and I was also asked about this during my recent interview on the Powerscripting Podcast last week. I admit that I have had this article in a draft status for close to a year now and actually forgot about it. So without further ado, here is my take on how to share variables and live data between runspaces in PowerShell.

I’ve shown in some articles various examples of using runspaces vs. PSJobs in your scripts and functions to achieve better performance and to make your UIs more fluid. What I haven’t really gotten into is another great feature of doing this which is to share your data between those runspaces or inject more data into a currently running runspace.

I would also like to leave a small disclaimer stating that this is an advanced technique and for anyone who is just getting into PowerShell, please do not get concerned if you do not understand what is going on or if it confusing. Just take it one command at a time and you will have it figured out in no time!

Synchronized Collections

The key component to all of this is done via a synchronized (thread safe) collection. There are multiple types of synchronized collections that include a hash table, arraylist, queue, etc… I typically use a synchronized hash table just because it is easier to call the variables or live objects by a name ($hash.name) versus using an arraylist or something similar and filtering for what I need using a Where-Object statement (($array | Where {$_.name –eq ‘Name’}).Value). Of course there may be cases when one might be better than another and in that case, always use the one that will help you out the most.

Creating a synchronized collection is pretty simply. This example will show how to create a synchronized hash table to use.

$hash = [hashtable]::Synchronized(@{})

All I had to do with use the [hashtable] type accelerator with the Synchronized() method and supply an empty hash table. Now I have a hash table that is synchronized and will work across runspaces regardless of the depth of the runspace.

Example of a PSJob with a synchronized hash table

First I want to show an example of using a synchronized hash table with a PSjob and see the outcome of the action.

Write-Host "PSJobs with a synchronized collection" -ForegroundColor Yellow -BackgroundColor Black $hash = [hashtable]::Synchronized(@{}) $hash.One = 1 Write-host ('Value of $Hash.One before PSjob is {0}' -f $hash.one) -ForegroundColor Green -BackgroundColor Black Start-Job -Name TestSync -ScriptBlock { Param ($hash) $hash.One++ } -ArgumentList $hash | Out-Null Get-Job -Name TestSync | Wait-Job | Out-Null Write-host ('Value of $Hash.One after PSjob is {0}' -f $hash.one) -ForegroundColor Green -BackgroundColor Black Get-Job | Remove-Job

As expected, after supplying the synchronized hash table to the PSJob via the –ArgumentList parameter, instead of seeing the expected value of 2 from the hash table, it is still only a 1. This is because of rolling another runspace in the background of the current session, a new PowerShell process is spawned and performs the work before returning any data back when using Receive-Job.

Now we will take a look at using a synched hash table with a single runspace and with multiple runspaces using a runspace pool because each of these approaches has a slightly different way of injecting the synched collection into a runspace.

Synchronized hash table with a single runspace

The first thing we are going to do is create the synched hash table and add a value of 1 to it.

$hash = [hashtable]::Synchronized(@{}) $hash.One = 1

Next I am going to create the runspace using the [runspacefactory] accelerator and using the CreateRunspace() method. Before I can add anything into the runspace, it must first be opened using the Open() method.

Write-host ('Value of $Hash.One before background runspace is {0}' -f $hash.one) -ForegroundColor Green -BackgroundColor Black $runspace = [runspacefactory]::CreateRunspace() $runspace.Open()

Now comes the moment where we will add the synched hash table into the runspace so that I can be accessed and used. Using the SetVariable() method of the $runspace.sessionstateproxy object, we add the synched hash table. We first give the a name of the variable and then add the actual synched collection.

$runspace.SessionStateProxy.SetVariable('Hash',$hash)

With that done, I then create the PowerShell instance and add my runspace object into it.

$powershell = [powershell]::Create() $powershell.Runspace = $runspace

Using the AddScript() method, I add the code that will actually happen within my background runspace when it is invoked later on. As I did with my PSJob example, I am only going to increment the hash table by one.

$powershell.AddScript({ $hash.one++ }) | Out-Null

The Out-Null at the end is used to prevent the output of the object that occurs.

Next, I actually kick off the runspace using BeginInvoke(). It is very important to save the output of this to a variable so you have a way to end the runspace when it has completed, especially when you are expecting to output some sort of object or other types of output. I also use my $handle variable to better track the state of the runspace so I can tell when it has finished.

$handle = $powershell.BeginInvoke() While (-Not $handle.IsCompleted) { Start-Sleep -Milliseconds 100 }

Once the runspace has completed, I then perform the necessary cleanup tasks. Note where I use my $handle variable with EndInvoke() to end the PowerShell instance.

$powershell.EndInvoke($handle) $runspace.Close() $powershell.Dispose()

Lastly, we can now see that the value did indeed increment in the runspace.

Write-host ('Value of $Hash.One after background runspace is {0}' -f $hash.one) -ForegroundColor Green -BackgroundColor Black

Pretty cool, right?

How about injecting more data or even viewing the state of the synched hash table while it is running? This can easily be done as well with the following example. I set up a loop within my background runspace that will run until I inject a boolean value to stop the loop. But before I do that, I will play with the data inside by seeing the value and then resetting it back to something else.

$hash = [hashtable]::Synchronized(@{}) $hash.value = 1 $hash.Flag = $True $hash.Host = $host Write-host ('Value of $Hash.value before background runspace is {0}' -f $hash.value) -ForegroundColor Green -BackgroundColor Black $runspace = [runspacefactory]::CreateRunspace() $runspace.Open() $runspace.SessionStateProxy.SetVariable('Hash',$hash) $powershell = [powershell]::Create() $powershell.Runspace = $runspace $powershell.AddScript({ While ($hash.Flag) { $hash.value++ $hash.Services = Get-Service $hash.host.ui.WriteVerboseLine($hash.value) Start-Sleep -Seconds 5 } }) | Out-Null $handle = $powershell.BeginInvoke()

I also added the parent host runspace into the child runspace so I can use the WriteVerboseLine() method to write the values to the parent session. I’ll sleep for 5 seconds to give me some time to inject values and read the values if needed. Also added is a call to Get-Service so I can pull live objects as well.

As you can see, the values continue to rise and I can even call $hash.value from my parent runspace to see the value or data. This can even include live objects if needed as seen below.

And now I inject some new data into the synched hash table to reset the counter.

Ok, enough fun here, time to close down the loop.

Don’t worry about the weird placement of the characters above. As long as you continue with the typing and hit return, the command will execute like you want it to.

That is really all with working with a single runspace. You can in fact go many many levels deep with background runspaces and use the same synched collection. The important thing to remember is that you must add the synched collection into each new runspace using the SetVariable() method that I talked about earlier.

Working with a runspace pool

I mentioned that working with synched collections is a little different with runspace pools than with a single runspace and now I will show you why. First I am going to set up my runspace pools with a helper function to handle the runspaces and also a scriptblock that will be added into the runspace pool.

Function Get-RunspaceData { [cmdletbinding()] param( [switch]$Wait ) Do { $more = $false Foreach($runspace in $runspaces) { If ($runspace.Runspace.isCompleted) { $runspace.powershell.EndInvoke($runspace.Runspace) $runspace.powershell.dispose() $runspace.Runspace = $null $runspace.powershell = $null } ElseIf ($runspace.Runspace -ne $null) { $more = $true } } If ($more -AND $PSBoundParameters['Wait']) { Start-Sleep -Milliseconds 100 } #Clean out unused runspace jobs $temphash = $runspaces.clone() $temphash | Where { $_.runspace -eq $Null } | ForEach { Write-Verbose ("Removing {0}" -f $_.computer) $Runspaces.remove($_) } [console]::Title = ("Remaining Runspace Jobs: {0}" -f ((@($runspaces | Where {$_.Runspace -ne $Null}).Count))) } while ($more -AND $PSBoundParameters['Wait']) } $ScriptBlock = { Param ($computer,$hash) $hash[$Computer]=([int]$computer*10) } $Script:runspaces = New-Object System.Collections.ArrayList $Computername = 1,2,3,4,5 $hash = [hashtable]::Synchronized(@{}) $sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault() $runspacepool = [runspacefactory]::CreateRunspacePool(1, 10, $sessionstate, $Host) $runspacepool.Open()

I only created an empty synched hash table because I will be adding data into it for each runspace in the runspace pool. With the runspace pool, I set the max concurrent runspaces running to 10 which will easily handle the 5 items that I want to add for each runspace.

Now I will iterate through each of the items and add them into the runspace pool.

ForEach ($Computer in $Computername) { #Create the powershell instance and supply the scriptblock with the other parameters $powershell = [powershell]::Create().AddScript($scriptBlock).AddArgument($computer).AddArgument($hash) #Add the runspace into the powershell instance $powershell.RunspacePool = $runspacepool #Create a temporary collection for each runspace $temp = "" | Select-Object PowerShell,Runspace,Computer $Temp.Computer = $Computer $temp.PowerShell = $powershell #Save the handle output when calling BeginInvoke() that will be used later to end the runspace $temp.Runspace = $powershell.BeginInvoke() Write-Verbose ("Adding {0} collection" -f $temp.Computer) $runspaces.Add($temp) | Out-Null }

As you can see, I use the AddArgument() parameter to add the synched hash table into the runspace. This works hand in hand with the Param() statement that I had in the scriptblock that was also added into the PowerShell instance using AddScript().

Calling the $hash.GetEnumerator() to show all of the values, we can see that I can view all of the data of each runspace (if it has written data to it yet).

$hash.GetEnumerator()

As expected again, I see the values from each runspace in the pool. Now I need to clean up after myself using my helper function.

Get-RunspaceData -Wait

Just like my other examples using the single runspace, I could inject values into the synched collection and monitor the synched collection. These are all simple examples that I have shown you but hopefully it is enough to take what I have shown you and expand out with more complex scripts/functions.

As always, I am interested to hear your thoughts and also any examples of what you have done using some of my techniques/examples that have been presented here.