|
| 1 | +--- |
| 2 | +RFC: RFC0023 |
| 3 | +Author: Bruce Payette |
| 4 | +Status: Draft |
| 5 | +SupercededBy: N/A |
| 6 | +Version: 0.1 |
| 7 | +Area: CMDLETS |
| 8 | +Comments Due: 4/10/2017 |
| 9 | +--- |
| 10 | + |
| 11 | +# Native Support for Concurrent Programming in PowerShell |
| 12 | + |
| 13 | +This RFC proposes a new built-in facility for concurrent programming in PowerShell. This facility will be implemented as a set of cmdlets on top of the existing runspace facility. |
| 14 | + |
| 15 | +## Motivation |
| 16 | + |
| 17 | +There are many scenarios in system administration where concurrent operations would be useful (see issue https://github.com/PowerShell/PowerShell/issues/3008). PowerShell workflow provides for concurrent operations but only within the context of a workflow thus entailing the constraints and overhead of the workflow runtime. There are a number of existing 3rd party implementations of similar functionality (e.g. https://github.com/proxb/PoshRSJob) The goal here is to provide a common in-box solution for concurrent execution in PowerShell scripts. |
| 18 | + |
| 19 | +### Goals |
| 20 | + |
| 21 | +Simplicity of use |
| 22 | +- Doesn’t use the jobs pattern which requires explicit cleanup for the job, Tasks can be fire and forget |
| 23 | + - Tasks will clean up after themselves, closing the runspace if appropriate and disposing the powershell object |
| 24 | + - Auto-dispose means no need for the equivalent to Remove-Job |
| 25 | +- No hierarchy as is the case with jobs/childjobs – one task object per call to Start-PSTask |
| 26 | +- No separate wait and receive commands. A single Wait-PSTask command subsumes both. |
| 27 | +Reliability: |
| 28 | +- Variables in task scriptblocks will need to either be explicitly initialized or have the $using: qualifier. Use of uninitialized variables will result in a parse-time error. |
| 29 | +- Stream (error, verbose, etc.) actions (-OnError, -OnVerbose, etc.) are supported to make sure output is not missed. |
| 30 | + |
| 31 | +## Specification |
| 32 | + |
| 33 | +New cmdlets Start-PSTask, Wait-PSTask, Stop-PSTask |
| 34 | + |
| 35 | +Start-PSTask [-ScriptBlock] <scriptblock> [-Arguments <Object[]>] [-Name <string>] [-OnError <scriptblock>] [-OnOutput <scriptblock>] [-OnVerbose <scriptblock>] [-OnInformation <scriptblock>] [-OnWarning <scriptblock>] [-OnDebug <scriptblock>] [<CommonParameters>] |
| 36 | +Start-PSTask [-FunctionName] <string> [-ArgumentList <Object[]>] [-Name <string>] [-OnError <scriptblock>] [-OnOutput <scriptblock>] [-OnVerbose <scriptblock>] [-OnInformation <scriptblock>] [-OnWarning <scriptblock>] [-OnDebug <scriptblock>] [<CommonParameters>] |
| 37 | + |
| 38 | +- This cmdlet starts a task instance and returns a PSTaskInfo object for that task. |
| 39 | +- Tasks will be identified by an integer ID which increases monotonically for each new task created |
| 40 | +- The maximum number of concurrently executing tasks will be controlled by a preference variable $PSTaskMaximumInstanceCount |
| 41 | +- Parameter -ScriptBlock |
| 42 | + - Specifies the scriptblock to use for the task’s actions. This scriptblock will be subject to strict variable assignment: variables must either be explicitly assigned in the scriptblock of have the `$using:` modifier. Use of unassigned variables will result in an error. |
| 43 | +- Parameter -FunctionName |
| 44 | + - Specifies the name of a function to use as the task’s action. Since $using: can’t be used in a function body, the -ArgumentList parameter should be used to pass arguments to the function. |
| 45 | +- Parameter -ArgumentList |
| 46 | + - Allows arguments to be passed to functions or parameterized scriptblocks. |
| 47 | +- Parameter -NoThrottle |
| 48 | + - allows a task to be run that doesn’t count against the maximum allowed number of concurrent tasks. |
| 49 | +? Example Scenario: background task colorizing the console that runs forever |
| 50 | +- Parameter -Name |
| 51 | + - if specified, the task id will use the name as the stem of the task id. E.g. if the name if “foo” then tasks will be named “foo.1”. “foo.2”, etc. |
| 52 | +- Parameter -On* |
| 53 | + - These parameters specify actions will be executed in the originating runspace, not in the task runspace. If there are any records in the associated stream, then the action scriptblock will be executed, receiving the PSTaskInfo object as $PSTaskInfo |
| 54 | +Stop-PStask -TaskList <PSTaskInfo[]> |
| 55 | +- Takes an array of PSTaskInfo objects and stops any tasks in the QUEUED or RUNNING states. Tasks in the STOPPED or COMPLETED state will not be impacted. |
| 56 | +- Returns nothing on success, but the task is placed in the STOPPED state. |
| 57 | +- If the task doesn’t exist, returns nothing |
| 58 | +- If the task fails to stop within as certain time window, returns an error |
| 59 | +- -Parameter -TaskList |
| 60 | + - Takes a list of PSTaskInfo objects to stop. |
| 61 | + - Supports ValueFromPipeline |
| 62 | +Wait-PSTask -Task <PSTaskInfo[]> [-Timeout NN] |
| 63 | +- Waits until the task object has completed, then writes content of all streams returned by the task to the corresponding output streams |
| 64 | +- Any pending task actions (OnError, On*) will be invoked prior to the task output being written. |
| 65 | +- Adds PSTaskID property to each object output |
| 66 | +- Parameter -TaskList |
| 67 | + - Takes a list of PSTaskInfo objects to wait for. |
| 68 | + - Supports ValueFromPipeline |
| 69 | +- Parameter -Timeout |
| 70 | + - Optional timeout for the wait in milliseconds |
| 71 | + |
| 72 | +A new class – PSTaskInfo – will be added. The PSTaskInfo class represents an instance of a task. A task can be in one of four states: QUEUED, RUNNING or COMPLETED or STOPPED. Tasks are queued when there aren’t enough runspaces to run all tasks concurrently (i.e. the value in $PSTaskMaximumInstanceCount has been reached). This class is defined as follows: |
| 73 | + |
| 74 | +```csharp |
| 75 | + public enum TaskState |
| 76 | + { |
| 77 | + QUEUED = 0, |
| 78 | + RUNNING = 1, |
| 79 | + COMPLETED = 2, |
| 80 | + STOPPED = 3, |
| 81 | + } |
| 82 | + |
| 83 | + public class PSTaskInfo |
| 84 | + { |
| 85 | + /// <summary> |
| 86 | + /// The current state of the task |
| 87 | + /// </summary> |
| 88 | + public TaskState State { get; set; } |
| 89 | + |
| 90 | + /// <summary> |
| 91 | + /// True if there were any errors during execution |
| 92 | + /// </summary> |
| 93 | + public bool HadErrors {get { … }} |
| 94 | + |
| 95 | + /// <summary> |
| 96 | + /// Wait for the task to complete then update the collections |
| 97 | + /// </summary> |
| 98 | + /// <returns></returns> |
| 99 | + public PSDataCollection<PSObject> Wait() { … } |
| 100 | + |
| 101 | + /// <summary> |
| 102 | + /// Identifier for the current task |
| 103 | + /// </summary> |
| 104 | + public string TaskID { get; set; } |
| 105 | + |
| 106 | + /// <summary> |
| 107 | + /// Content of the streams saved as lists so they can be inspected without |
| 108 | + /// draining the collection. Must be typed as PSObject to allow the PSTaskID to |
| 109 | + /// be added to the result object. |
| 110 | + /// </summary> |
| 111 | + public List<PSObject> Output { get; set; } |
| 112 | + public List<PSObject> Error { get; set; } |
| 113 | + public List<PSObject> Verbose { get; set; } |
| 114 | + public List<PSObject> Warning { get; set; } |
| 115 | + public List<PSObject> Debug { get; set; } |
| 116 | + public List<PSObject> Information { get; set; } |
| 117 | + |
| 118 | + } |
| 119 | +``` |
| 120 | + |
| 121 | +### New Preference variable |
| 122 | +```powershell |
| 123 | + $PSTaskThrottleLimit |
| 124 | +``` |
| 125 | +This preference variable controls the maximum number of concurrently running tasks. This limit can be bypassed by using the -NoThrottle parameter on Start-PSTask. |
| 126 | + |
| 127 | +### Execution Environment |
| 128 | + |
| 129 | +Tasks will be executed in the same process/appdomain as the parent. The execution environment for a throttled task will be equivalent to runspace pool runspace. This implies that there is no shared state between the parent and task runspaces. Any modules required by the task will either need to be explicitly loaded or be loaded through autoloading. The current working directory for the runspace will be whatever it was set to when last used. |
| 130 | +Tasks started with -NoThrottle will execute in process in a new runspace created by the equivalent of RunspaceFactory.CreateRunspace(). |
| 131 | + |
| 132 | +### Examples |
| 133 | + |
| 134 | +```powershell |
| 135 | + #=================================== |
| 136 | + # Simple single task execution |
| 137 | + # |
| 138 | + Start-PSTask { "Hello" } | Wait-PSTask |
| 139 | + |
| 140 | + #=================================== |
| 141 | + # Use with Foreach-Object |
| 142 | + # |
| 143 | + 1..20 | foreach {Start-PSTask { "Hello $using:_" }} | Wait-PSTask |
| 144 | + |
| 145 | + #==================================== |
| 146 | + # Use ForEach-Object with -Arguments |
| 147 | + # |
| 148 | + 1..10 | foreach {Start-PSTask {param($x) "x is $x"} -Arguments $_} | wait-pstask |
| 149 | + |
| 150 | + #=================================== |
| 151 | + # Example using foreach statement to get sizes of files into a hashtable |
| 152 | + # |
| 153 | + $tl = foreach ($f in Get-ChildItem *.ps1) { |
| 154 | + Start-PSTask { |
| 155 | + # Return a hashtable mapping leaf name to count |
| 156 | + @{ (Split-Path -Leaf $using:f) = (cat $using:f).Count } |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + $result = @{} |
| 161 | + # Add all of the hashtables together... |
| 162 | + foreach ($r in Wait-PSTask $tl) |
| 163 | + { |
| 164 | + $result += $r |
| 165 | + } |
| 166 | + # And return the aggregate hash table... |
| 167 | + $result |
| 168 | + |
| 169 | + #==================================== |
| 170 | + # launch parallel tasks in a foreach loop, computing the |
| 171 | + # factorial of the iteration number |
| 172 | + # |
| 173 | + $tasks = foreach ($iteration in 1..10) { |
| 174 | + Start-PSTask { |
| 175 | + |
| 176 | + function fact ($x) |
| 177 | + { |
| 178 | + (1..$x).foreach{ |
| 179 | + begin {$result = 1} |
| 180 | + process {$result *= $_ } |
| 181 | + end { $result }} |
| 182 | + } |
| 183 | + |
| 184 | + if ($using:iteration -eq 3) |
| 185 | + { |
| 186 | + Write-Error "Error in iteration 3" |
| 187 | + } |
| 188 | + # Write directly to console for immediate display |
| 189 | + |
| 190 | + [console]::WriteLine( |
| 191 | +"Fact $using:iteration is $(fact $using:iteration)") |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + # Handle the results of the tasks... |
| 196 | + foreach ($t in $tasks) |
| 197 | + { |
| 198 | + Wait-PSTask $t |
| 199 | + if ($t.HadErrors) |
| 200 | + { |
| 201 | + $t.Error |
| 202 | + } |
| 203 | + } |
| 204 | + |
| 205 | +#==================================== |
| 206 | +# start three individual tasks |
| 207 | +$t1 = start-pstask { "Hello from task1" } |
| 208 | +$t2 = start-pstask { "Hello from task2" } |
| 209 | +$t3 = start-pstask { "Hello from task3" } |
| 210 | +# wait for the results of all three |
| 211 | +Wait-PSTask $t1,$t2,$t3 |
| 212 | +``` |
| 213 | + |
| 214 | +## Alternate Proposals and Considerations |
| 215 | + |
| 216 | +None. |
| 217 | + |
0 commit comments