Yet another multi threading script for Powershell

First I must say I think it’s awesome to get a comment from someone on the PoSH team. Thanks.

Now, about the script, I must criticize/comment on the inspiration for this script. The original inspiration is Karl Prosser’s *-backgroundpipeline snap in. That snap in, for some reason, didn’t work for me when it was new, and now it’s out of date. Due to the lack of function of it I tried to write a script to fill the hole in my heart. That script worked, but it sucked. So I had to re script the whole thing, but before I did that I looked for one that works and is easier to use. I did a very short search and found Jim Truher’s New-Job script. The New-Job script is an attempt to copy the background job feature of bash. The New-Job script got a vague appearance of that feature, however bash allows as many background jobs as you want, the script did not.

My script is a library script with seven functions. Three are used to control the runspace objects that are necessary for multi threading in Powershell, and the other four are for controlling pipeline execution(commands). The runspaces can be referenced by a number returned when it is created, or by a name given when it is created. Wild card characters can be given to the Get-AsyncRunspace function to references to several runspaces. The Invoke-ExpressionInRunspace function is simplified function of the other pipeline functions, and executes the given command syncronusly with the current thread, making it more efficient with small commands. The Read-AsyncPipe function is optional and can be used to get the results of a command as it executes.

I hope you find it easy to use if you do use it. I’ll sugjest using three letter aliases, in place of the long function names, when you’re using the shell in interactive mode. Of course, each runspace keeps track of their own variables and you could pass reference objects between them, and overall do most of what you could do with any other .NET, multi threaded programming language.

Now the script.

file: Library-AsyncRunspace.ps1

# Library-AsyncRunspace
# Version: 1.1

# Include with {. Library-AsyncRunspace}

# Sugjested aliases...
##New-Alias eap End-AsyncPipe
##New-Alias gar Get-AsyncRunspace
##New-Alias r! Invoke-ExpressionInRunspace
##New-Alias iar Invoke-ExpressionInRunspace
##New-Alias nar New-AsyncRunspace
##New-Alias rap Read-AsyncPipe
##New-Alias rar Remove-AsyncRunspace
##New-Alias sap Start-AsyncPipe

# Don't destroy previous runspace catalog
if ( -not $RunspaceCatalog ) {
	$RunspaceCatalog = New-Object PSObject
	$RunspaceCatalog | Add-Member NoteProperty RunspaceCountPosition 0 -PassThru |
		Add-Member NoteProperty RunspaceList @{}
}

function New-AsyncRunspace {
	param ([String]$NewName)
	
	# Incroment id number counter
	$RunspaceCatalog.RunspaceCountPosition++
	
	# Generate a runspace and add usefull properties
	$NewRunspace = [management.automation.runspaces.runspacefactory]::CreateRunspace() |
		Add-Member NoteProperty Name $NewName -PassThru |
		Add-Member NoteProperty CurrentPipe $null -PassThru |
		Add-Member ScriptProperty PipeState { $this.CurrentPipe.PipelineStateInfo.State } -PassThru |
		Add-Member ScriptProperty PipeStateReason { $this.CurrentPipe.PipelineStateInfo.Reason } -PassThru |
		Add-Member ScriptProperty CurrentCommands { "{$($this.CurrentPipe.Commands)}" } -PassThru
	$NewRunspace.Open()
	
	# Add new runspace to catalog
	$RunspaceCatalog.RunspaceList[$RunspaceCatalog.RunspaceCountPosition] = $NewRunspace
	
	# Report new runspace id number
	$RunspaceCatalog.RunspaceCountPosition
}

function Remove-AsyncRunspace {
	param ($RunspaceID = (read-host "Enter a valid runspace ID or name."))

	if ( -not ($RunspaceID -is [Int32] -or $RunspaceID -is [String])) { throw "A runspace id must be supplied by integer or name!" }
	
	#Find selected runspace by number
	if ($RunspaceID -is [Int32]) {
		$RunspaceCatalog.RunspaceList[$RunspaceID].Close()
		$RunspaceCatalog.RunspaceList.Remove($RunspaceID)
	}
	#Or find selected runspace by name
	else {
		$keys = $RunspaceCatalog.RunspaceList.Keys
		$keys | Foreach-Object {
			if ( $RunspaceCatalog.RunspaceList[$_].Name -like $RunspaceID ) {
				$RunspaceCatalog.RunspaceList[$_].Close()
				$RunspaceCatalog.RunspaceList.Remove($_)
			}
		}
	}
}

function Get-AsyncRunspace {
	param ($RunspaceID)
	
	# Return the entire list of available runspaces by default
	if ( $RunspaceID -eq $null ) {
		return $RunspaceCatalog.RunspaceList.Values
	}
	
	# Type check on id
	if ( -not ($RunspaceID -is [Int32] -or $RunspaceID -is [String])) { throw "A runspace id must be supplied by integer or name!" }
	
	#Find selected runspace by number
	if ($RunspaceID -is [Int32]) {
		$SelectedRunspace = $RunspaceCatalog.RunspaceList[$RunspaceID]
	}
	#Or find selected runspace by name
	else {
		$SelectedRunspace = $RunspaceCatalog.RunspaceList.Values | Where-Object { $_.Name -like $RunspaceID }
	}
	
	# Return result
	$SelectedRunspace
}

function Invoke-ExpressionInRunspace {
	param ($RunspaceID = (read-host "Enter a valid runspace ID or name."), [String]$Command = (read-host "Enter a command to be executed in the runspace."))
	
	$SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1
	
	# Throw error if no runspace was found with the given id
	if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }
	
	# Insert the command into the runspace.
	$Pipeline = $SelectedRunspace.CreatePipeline($Command)

	# Insert input into the new pipe
	$input | Foreach-Object { [void]$Pipeline.Input.Write($_) }
	$Pipeline.Input.Close()
	
	# Execute pipe
	$Pipeline.Invoke()
	
	# Return output from pipe
	$Pipeline.Output.ReadToEnd()
}

function Start-AsyncPipe {
	param (
		$RunspaceID = (read-host "Enter a valid runspace ID or name."),
		[String]$Command = (read-host "Enter a command to executed in the runspace.")
	)

	BEGIN {
		$SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1
		
		# Throw error if no runspace was found with the given id
		if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }
		
		# Insert the command into the runspace.
		$Pipeline = $SelectedRunspace.CreatePipeline($Command)
		
		# Start async process
		$Pipeline.InvokeAsync()
		
		# List this pipe as current with the runspace
		$SelectedRunspace.CurrentPipe = $Pipeline
	}
	
	PROCESS {
	# Insert input into the new pipe
	[void]$Pipeline.Input.Write($_)
	}
	
	END {
	# Close input
	$Pipeline.Input.Close()
	}
}

function Read-AsyncPipe {
	param ($RunspaceID = (read-host "Enter a valid runspace ID or name."))
	$SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1

	# Throw error if no runspace was found with the given id
	if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }

	# Read pipeline result
	if ($SelectedRunspace.CurrentPipe.pipelineStateInfo.state -eq [management.automation.runspaces.pipelinestate]::failed) {
		throw $SelectedRunspace.CurrentPipe.PipelineStateInfo.Reason
	}
	$SelectedRunspace.CurrentPipe.Output.nonblockingread()
}

function End-AsyncPipe {
	param ($RunspaceID = (read-host "Enter a valid runspace ID or name."))

	$SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1
	
	# Throw error if no runspace was found with the given id
	if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }

	# Pipeline is no longer current
	$Pipeline = $SelectedRunspace.CurrentPipe
	$SelectedRunspace.CurrentPipe = $null

	# Read pipeline result
	if ($Pipeline.PipelineStateInfo.State -eq [management.automation.runspaces.pipelinestate]::failed) {
		throw $Pipeline.PipelineStateInfo.Reason
	}
	else {
		while (-not $Pipeline.Output.endofpipeline) {
			$Pipeline.Output.nonblockingread()
			start-sleep -m 50   #don't overload the cpu
		}
	$Pipeline.output.Close()
	}
}

That’s it. 183 lines of code and it can’t do a thing with out your inspiration so take it and build some network applications. I’ll have to make a note of those three letter aliases again, because learning the dynamics of the whole thing using the full names is less than fun. I did enjoy the debug procedure using those aliases and funny runspace names.

Now I’ll close with a simple example of the use of the script. The following example should be valid is you’re using the aliases.
PS>nar foo
1
PS>iar foo { $h = "Hello"; $w = "world!" }
PS>"$(iar foo {$h}) $(iar foo {$w})"
Hello world!

Try it. Have fun, and all that will make sence in about 5 minutes.

Advertisements

~ by lunaticexperimentalist on December 10, 2006.

2 Responses to “Yet another multi threading script for Powershell”

  1. Hi. I try to use your library but run into the problem that I cannot pass a variable to a runspace. How can I do that?$thread = New-AsyncRunspace$test = "1234"Invoke-ExpressionInRunspace 1 { $test2 = $test }"$(Invoke-ExpressionInRunspace 1 {$test2})"The result is empty.

  2. Runspaces do not share variables, so you will need to pipe data and objects to other runspaces to be able to use them.Here is an example of pipeing a string into the runspace with the ID of 1, that runspace can then work with that string and pipe some data out, or do anything else that can be done in Powershell.PS> ‘asdf’ | Invoke-ExpressionInRunspace 1 { @($input)[0]+’qwer’ }asdfqwer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: