I have a PowerShell script where I want to create a background thread and dynamically exchange data with my primary thread. The idea was to use the information stream since it can handle all kind of objects easily.
Usually I do so by giving the PowerShell-Object to itself like the following:
$Code =
{
Param($Me)
#Here I can use $Me.Streams.Information to exchange data any time,
#for example to feed my thread with more work to do on the fly
$ResultData = [System.Object[]]::new(0)
$WorkCounter = 0
$Finished = $false
while (-not $Finished)
{
while ($Me.Streams.Information.Count -eq $WorkCounter)
{
#Wait for data to be added to the information stream
Sleep -MilliSeconds 10
}
$InputData = $Me.Streams.Information[-1].MessageData
if ($InputData -eq "FINISHED")
{
$Finished = $true
}
else
{
<# Do some stuff with the $InputData #>
$ResultData = $ProgressedInputData
}
$WorkCounter
}
Write-Information $ResultData
}
$PS = [PowerShell]::Create()
$PS.AddScript($Code) | Out-Null
$PS.AddArgument($PS) | Out-Null #Hand the PS to itself to make the streams accessible inside the thread
$Handle = $PS.BeginInvoke() | Out-Null
for ($i = 0; $i -lt 10; $i )
{
$PS.Streams.Information.Add([System.Management.Automation.InformationRecord]::new($i, ""))
#I just gave my background thread some stuff to do without the need to instantiate a new one again
#Now this thread can do some work too...
}
$PS.Streams.Information.Add([System.Management.Automation.InformationRecord]::new("FINISHED", ""))
$Handle.AsyncWaitHandle.WaitOne() #Wait for my background thread to finish all its work
$SomeReturnValue = $PS.Streams.Information[-1].MessageData
My actual question is: Is it possible, to access the current PowerShell instance without the need to hand it over like I did with $PS.AddArgument($PS)?
CodePudding user response:
You don't need to abuse PowerShell.Streams
for two-way communication here - PowerShell already has a facility for bridging session state between two runspaces, a so-called Session State Proxy!
Let's start with a slight rewrite of your $code
block:
$code = {
while($config['Enabled']){
$inputData = $null
if($queue.TryDequeue([ref]$inputData)) {
#process input data
Write-Information $inputData
}
else {
Start-Sleep -Milliseconds 50
}
}
}
Notice that I'm using two variables, $config
and $queue
, even though I haven't actually parameterized or otherwise defined them, but we're still using the Information stream to communicate output (you could use standard output as well).
Now we just need to create two objects that we can bind to those variables. For thread-safety, we'll be using:
- A
ConcurrentQueue[psobject]
for the input data - A thread-synchronized
[hashtable]
for the config data
# Create PowerShell instance like before
$PS = [powershell]::Create()
# Create the thread-safe collections we'll be using to communicate
$queue = [System.Collections.Concurrent.ConcurrentQueue[psobject]]::new()
$config = [hashtable]::Synchronized(@{
Enabled = $true
})
# Now make those variable references available in the runspace where the backgroun code will be running
$ps.Runspace.SessionStateProxy.PSVariable.Set('queue', $queue)
$ps.Runspace.SessionStateProxy.PSVariable.Set('config', $config)
With a facility for exchanging both input and configuration data, you can now invoke the background job and observe how it behaves based on the input provided via the $queue
(I strongly suggest entering the following statements into an interactive prompt, play around with it a bit):
# Invoke background code
$asyncHandle = $PS.BeginInvoke()
# Try adding some data to the queue
$queue.TryAdd([pscustomobject]@{ Property = "Value 123"})
# Wait for a bit
Start-Sleep -Milliseconds 100
# Observe that the queue has been emptied by the background code
Write-Host "Queue is empty: $($queue.IsEmpty)"
# Observe that the background code actually processed (and output) the data
$ps.Streams.Information
CodePudding user response:
Let me offer an alternative to Mathias R. Jessen's helpful answer, based on the ThreadJob
module's Start-ThreadJob
cmdlet, which ships with PowerShell (Core) v6 and in Windows PowerShell can be installed on demand (e.g., Install-Module ThreadJob -Scope CurrentUser
)
As the name suggests, it offers thread-based background operations, as a faster and lighter-weight alternative to the child-process-based background jobs created by Start-Job
(see this answer for a juxtaposition).
As such, it is a friendlier, higher-level alternative to managing multiple threads (runspaces) via the PowerShell SDK, allowing you to use the usual job-management cmdlets to interact with the background threads.
A simple example:
# Create a synchronized (thread-safe) queue.
$threadSafeQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]::new()
# Start a thread job that keeps processing elements in the queue
# indefinitely, sleeping a little between checks for new elements.
# A special element value is used to signal that processing should end.
$jb = Start-ThreadJob {
$q = $using:threadSafeQueue # get a reference to the thread-safe queue.
$element = $null # variable to receive queue elements
while ($true) {
# Process all elements currently in the queue.
while ($q.TryDequeue([ref] $element)) {
# Check for the signal to quit, by convention a single NUL char. here.
if ("`0" -eq $element) { 'Quitting...'; return }
# Process the element at hand.
# In this example, echo the dequeued element enclosed in "[...]"
'[{0}]' -f $element
}
# Queue is (now) empty, sleep a little before checking for new elements.
Start-Sleep -MilliSeconds 100
}
}
# Populate the queue with the numbers from 1 to 10.
1..10 | ForEach-Object {
$threadSafeQueue.Enqueue($_) # This triggers activity in the background thread.
# Retrieve available output from the thread job.
$jb | Receive-Job
}
# Send the quit signal, retrieve remaining output and delete the job.
$threadSafeQueue.Enqueue("`0")
$jb | Receive-Job -Wait -AutoRemoveJob
Output:
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10]
Quitting...
See also:
- The PowerShell (Core) v7
Foreach-Object -Parallel
feature, which similarly uses threads to process pipeline input in parallel.