{"id":189,"date":"2014-06-24T15:12:49","date_gmt":"2014-06-24T21:12:49","guid":{"rendered":"http:\/\/www.Get-Blog.com\/?p=189"},"modified":"2014-06-24T15:22:21","modified_gmt":"2014-06-24T21:22:21","slug":"true-multithreading-in-powershell","status":"publish","type":"post","link":"http:\/\/www.Get-Blog.com\/?p=189","title":{"rendered":"True Multithreading in PowerShell"},"content":{"rendered":"<p>As with all things, the longer you play with something the more you learn about it.  It has been nearly 5 years since I wrote my <a href=\"http:\/\/www.get-blog.com\/?p=22\">original article<\/a> on multithreading where I used PowerShell jobs to run multiple items at a time.  In the conversations following that post I had a reader submit a fairly nasty note saying that multithreading with jobs was in fact not real multithreading.  He also selected some choice words for me making sure I couldn&#8217;t post his comments.  It hurt my very fragile feelings and, as a result, I started looking at how I could really, truly multithread with PowerShell.  The result of that effort is this script.  This is a true multithreading script no \u201cifs\u201d, \u201cands\u201d or \u201cbuts\u201d.<\/p>\n<p>It runs MUCH faster than my other rendition and includes a much more advanced feature set from my other script.  The down side is that the script is very difficult to understand if you are new to PowerShell.  If you would like to have a script that is more visible and easier to understand, please refer to the <a href=\"http:\/\/www.get-blog.com\/?p=22\">version that uses Jobs<\/a>.<\/p>\n<p>Okay, so the big addition for this script is the ability to either run a script or a cmdlet that\u2019s built in.  As well, you can run this within the pipeline!  To do that I had to include the begin, process and end blocks.  It makes the script a bit more complex, but really pays off when you pipe your custom script into <span class=\"lang:ps decode:true  crayon-inline\">Out-Gridview<\/span> or pipe your advanced filtering script into a multithreaded one!  I have even pulled my SCCM collections via <span class=\"lang:ps decode:true  crayon-inline\">Get-WmiObject<\/span> and piped them into a multithreaded script!  Cool stuff!<\/p>\n<p>Okay, so on to the breakdown.<\/p>\n<p>First we need to get all of our parameters.  If this doesn\u2019t make since, please find my post on <a href=\"http:\/\/www.get-blog.com\/?p=27\">parameters<\/a>!  <\/p>\n<pre class=\"lang:ps decode:true\">Param($Command = $(Read-Host \"Enter the script file\"), \r\n    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]$ObjectList,\r\n    $InputParam = $Null,\r\n    $MaxThreads = 20,\r\n    $SleepTimer = 200,\r\n    $MaxResultTime = 120,\r\n    [HashTable]$AddParam = @{},\r\n    [Array]$AddSwitch = @()\r\n) <\/pre>\n<p>All of these are defined as follows:<br \/>\n<strong>Command<\/strong><br \/>\nThis is where you provide the powershell Commandlet \/ Script file that you want to multithread.  You can also choose a built in cmdlet.  Keep in mind that your script.  This script is read into a scriptblock, so any unforeseen errors are likely caused by the conversion to a script block.<\/p>\n<p><strong>ObjectList<\/strong><br \/>\nThe objectlist represents the arguments that are provided to the child script.  This is an open ended argument and can take a single object from the pipeline, an array, a collection, or a file name.  The multithreading script does it&#8217;s best to find out which you have provided and handle it as such.  If you would like to provide a file, then the file is read with one object on each line and will be provided as is to the script you are running as a string.  If this is not desired, then use an array.<\/p>\n<p><strong>InputParam<\/strong><br \/>\nThis allows you to specify the parameter for which your input objects are to be evaluated.  As an example, if you were to provide a computer name to the Get-Process cmdlet as just an argument, it would attempt to find all processes where the name was the provided computer name and fail.  You need to specify that the parameter that you are providing is the &#8220;ComputerName&#8221;.<\/p>\n<p><strong>AddParam<\/strong><br \/>\nThis allows you to specify additional parameters to the running command.  For instance, if you are trying to find the status of the &#8220;BITS&#8221; service on all servers in your list, you will need to specify the &#8220;Name&#8221; parameter.  This command takes a hash pair formatted as follows:  <\/p>\n<pre class=\"theme:psconsole lang:ps decode:true\">@{\"ParameterName\" = \"Value\"}\r\n@{\"ParameterName\" = \"Value\" ; \"ParameterTwo\" = \"Value2\"}<\/pre>\n<p><strong>AddSwitch<\/strong><br \/>\nThis allows you to add additional switches to the command you are running.  For instance, you may want to include &#8220;RequiredServices&#8221; to the &#8220;Get-Service&#8221; cmdlet.  This parameter will take a single string, or an aray of strings as follows:<\/p>\n<pre class=\"theme:psconsole lang:ps decode:true\">\"RequiredServices\"\r\n@(\"RequiredServices\", \"DependentServices\")<\/pre>\n<p><strong>MaxThreads<\/strong><br \/>\nThis is the maximum number of threads to run at any given time.  If resources are too congested try lowering this number.  The default value is 20.<\/p>\n<p><strong>SleepTimer<\/strong><br \/>\nThis is the time between cycles of the child process detection cycle.  The default value is 200ms.  If CPU utilization is high then you can consider increasing this delay.  If the child script takes a long time to run, then you might increase this value to around 1000 (or 1 second in the detection cycle).<\/p>\n<p>Now we need to set everything up.  This stuff needs to execute outside of the pipeline, so we place it in the \u201cbegin\u201d block of the script.<\/p>\n<pre class=\"lang:ps decode:true\">Begin{\r\n    $ISS = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()\r\n    $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads, $ISS, $Host)\r\n    $RunspacePool.Open()\r\n        \r\n    If ($(Get-Command | Select-Object Name) -match $Command){\r\n        $Code = $Null\r\n    }Else{\r\n        $OFS = \"`r`n\"\r\n        $Code = [ScriptBlock]::Create($(Get-Content $Command))\r\n        Remove-Variable OFS\r\n    }\r\n    $Jobs = @()\r\n} <\/pre>\n<p>Okay, so to break this down, first we need to make our ISS, or initial session state.  This is basically the session state to be used when we open our Runspace.  Next we create our Runspaces.  The RunspacePool is really what\u2019s going to do the multithreading.  It will handle starting our threads and continuously start new ones as required.  It is the operating environment for our command pipeline.  Finally we open the RunSpacePool.  Note that \u201c.Open()\u201d opens the RunSpacePool synchronously, creating a Windows PowerShell execution environment. <\/p>\n<p>Now I am running a detection on what the user provided for the <span class=\"lang:ps decode:true  crayon-inline\">$command<\/span> parameter.  First I will look at all of the currently loaded cmdlets.  If it is one of those, then we continue.  Otherwise we assume it is a script file.  If it is a script file then we need to read the file in to a script block that we can pass to our future threads.  To do this we need to change the default <span class=\"lang:ps decode:true  crayon-inline\">$OFS<\/span> (Object Field Separator), for more understanding here, please read my <a href=\"http:\/\/www.get-blog.com\/?p=182\">other post<\/a>!<\/p>\n<p>Okay, so the next step is to start receiving items from the pipeline.  We can do this by starting the process block.  Note that the process block is executed for each item we find in the pipeline.   Meanwhile, if you did not execute in the pipeline it is executed once for the script as a whole.  What this means is that we need to assume that <span class=\"lang:ps decode:true  crayon-inline\">$ObjectList<\/span> will either be a single item or multiple items.  The best way to do that is to use a <span class=\"lang:ps decode:true  crayon-inline\">ForEach<\/span> Loop.<\/p>\n<pre class=\"lang:ps decode:true\">Process{\r\n    Write-Progress -Activity \"Preloading threads\" -Status \"Starting Job $($jobs.count)\"\r\n    ForEach ($Object in $ObjectList){\r\n        If ($Code -eq $Null){\r\n            $PowershellThread = [powershell]::Create().AddCommand($Command)\r\n        }Else{\r\n            $PowershellThread = [powershell]::Create().AddScript($Code)\r\n        }\r\n        If ($InputParam -ne $Null){\r\n            $PowershellThread.AddParameter($InputParam, $Object.ToString()) | out-null\r\n        }Else{\r\n            $PowershellThread.AddArgument($Object.ToString()) | out-null\r\n        }\r\n        ForEach($Key in $AddParam.Keys){\r\n            $PowershellThread.AddParameter($Key, $AddParam.$key) | out-null\r\n        }\r\n        ForEach($Switch in $AddSwitch){\r\n            $Switch\r\n            $PowershellThread.AddParameter($Switch) | out-null\r\n        }\r\n        $PowershellThread.RunspacePool = $RunspacePool\r\n        $Handle = $PowershellThread.BeginInvoke()\r\n        $Job = \"\" | Select-Object Handle, Thread, object\r\n        $Job.Handle = $Handle\r\n        $Job.Thread = $PowershellThread\r\n        $Job.Object = $Object.ToString()\r\n        $Jobs += $Job\r\n    }\r\n        \r\n} <\/pre>\n<p>So now we have to build the thread that we are going to execute.  We do this by adding either the command, or the script.  The first IF block is to determine which.  A thread either takes an existing PowerShell command, or a Scriptblock.  If you remember we built this out in the <span class=\"lang:ps decode:true  crayon-inline\">Begin<\/span> statement above.  Once that is done we need to start giving the user the power to control the item we are calling. <\/p>\n<p>First things first we look at <span class=\"lang:ps decode:true  crayon-inline\">$InputParam<\/span>.  This is what allows the user to execute the child script not just with the argument provided, but also specify the parameter.  We see this is useful with the <span class=\"lang:ps decode:true  crayon-inline\">Get-Process<\/span> cmdlet.  Let\u2019s say that you want to see the processes running on 20 different servers.  If you just ran <span class=\"lang:ps decode:true  crayon-inline\">Get-Process ServerName<\/span> you would be looking at your local machine for any processes with the name \u201cServerName\u201d and you would get no return (probably).  Instead you would want to run <span class=\"lang:ps decode:true  crayon-inline\">Get-Process \u2013ComputerName ServerName<\/span>.  The trick here is that when you do this you\u2019ve actually changed things!  When an item is just hanging at the end of the statement, it is called an Argument.  When you pair the item you are setting with the setting it is called a Parameter.  So if the user wants to specify the parameter name, we are actually adding a different item to our thread!<\/p>\n<p>Now we need to see if the user wanted to add some extra parameters.  For this I decided that a hash table was perfect.  This is because they are built much like a parameter, as they are name \/ value pairs.  The user can provide as many as they want in a single hash table, and we can easily run a <span class=\"lang:ps decode:true  crayon-inline\">ForEach<\/span> to evaluate this.  Again we are going to use the .AddParameter() statement to evaluate.<\/p>\n<p>Finally we need to add any Switches that the user wants to add.  A good example of this is the \u2013Force switch on cmdlets like <span class=\"lang:ps decode:true  crayon-inline\">Get-ChildItem<\/span>.  We can do this with an array of string which again allows the user to put in as many various switches as they like.  One interesting not here is that I had to use .AddParameter() for this as well.  Instead of creating a method called .AddSwitch(), Microsoft simply chose to add an overloaded definition of .AddParameter().  What\u2019s peculiar is that even in their documentation (link below) it does in fact say that providing just a string adds a switch instead of parameter.<\/p>\n<p>Now that we have out thread all set up we simply attach our RunspacePool that we setup in the begin statement, and then tell it to execute our thread!<\/p>\n<p>To avoid having a thread hanging around that we lose track of we need to try to do some tracking here.  To do this we first catch the thread by creating the output of the command in a <span class=\"lang:ps decode:true  crayon-inline\">$Handle<\/span> variable.  This will provide the link back to the handle in our \u201cEnd\u201d block.  I then go ahead and create a custom object which I have named <span class=\"lang:ps decode:true  crayon-inline\">$Job<\/span> to hold all these little gems of knowledge.  Then I add my custom object to the array for tracking called <span class=\"lang:ps decode:true  crayon-inline\">$Jobs<\/Span>.  This method of creating objects was taught to me by my reader from <a href=\"http:\/\/www.get-blog.com\/?p=82\">this post<\/a>!<\/p>\n<p>At this point all of the code to start the jobs is complete!  Now we just have to grab all of the jobs back.  That calls for the \u201cEnd\u201d block which is executed once per script run.  Since that is the case, we need one main loop to ensure that it stay running while we need it to.  Within the &#8220;End&#8221; block we will watch for jobs to finish and provide that output as they complete.<\/p>\n<pre class=\"lang:ps decode:true\">End{\r\n    $ResultTimer = Get-Date\r\n    While (@($Jobs | Where-Object {$_.Handle -ne $Null}).count -gt 0)  {\r\n    \r\n        $Remaining = \"$($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).object)\"\r\n        If ($Remaining.Length -gt 60){\r\n            $Remaining = $Remaining.Substring(0,60) + \"...\"\r\n        }\r\n        Write-Progress `\r\n            -Activity \"Waiting for Jobs - $($MaxThreads - $($RunspacePool.GetAvailableRunspaces())) of $MaxThreads threads running\" `\r\n            -PercentComplete (($Jobs.count - $($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).count)) \/ $Jobs.Count * 100) `\r\n            -Status \"$(@($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False})).count) remaining - $remaining\" \r\n\r\n        ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})){\r\n            $Job.Thread.EndInvoke($Job.Handle)\r\n            $Job.Thread.Dispose()\r\n            $Job.Thread = $Null\r\n            $Job.Handle = $Null\r\n            $ResultTimer = Get-Date\r\n        }\r\n        If (($(Get-Date) - $ResultTimer).totalseconds -gt $MaxResultTime){\r\n            Write-Error \"Child script appears to be frozen, try increasing MaxResultTime\"\r\n            Exit\r\n        }\r\n        Start-Sleep -Milliseconds $SleepTimer\r\n        \r\n    } \r\n    $RunspacePool.Close() | Out-Null\r\n    $RunspacePool.Dispose() | Out-Null\r\n} <\/pre>\n<p>The first part of this is simply to provide some form of pretty output.  First, I evaluate what jobs are still running and creating a string (truncated) that names them.  I added this so that if the child script is failing on just one or two servers, you will known which they are to fix them.  Then a rather complex write-progress statement which I\u2019ll let you look into.  For More info on write-progress you can read my blog articles over <a href=\"http:\/\/www.get-blog.com\/?p=168\">write-progress<\/a> or <a href=\"http:\/\/www.get-blog.com\/?p=132\">getting the progress of a child job<\/a>.<\/p>\n<p>Okay, so now we look for jobs that are completed.  We can do this by looking into our array of objects where our handle (that we captured above) has .IsCompleted set to <span class=\"lang:ps decode:true  crayon-inline\">$true<\/span>!  We then run a <span class=\"lang:ps decode:true  crayon-inline\">ForEach<\/span> on each of these to stop it running and dispose of it.  Keep in mind that dispose returns all of the output from the thread.  What this means is that the script will actually write all of the output as jobs are finished instead of having to wait for all jobs to finish!<\/p>\n<p>Next I added some protection to the script.  I had a couple of scripts in my arsenal that would lock up and never finish.  When this happened the multithreading script would continue to run and loop forever and ever.  To stop this I added a maximum time to wait for additional jobs to finish.  To do this I simply look at the system clock for when the last job completed and look at the time gap.  If it is greater than out parameter for <span class=\"lang:ps decode:true  crayon-inline\">$MaxResultTime<\/span> then we\u2019ll throw an error and exit.  Note that until PowerShell actually closes those threads will continue to hang around!<\/p>\n<p>As a very last step we clean up our <span class=\"lang:ps decode:true  crayon-inline\">$RunspacePool<\/span>.<\/p>\n<p>Well that\u2019s all folks!  I really hope that this script provide many time saving events for my wonderful readers.  I know it has saved me hundreds of man hours!<\/p>\n<p>Following is the full script with the comment block intact for your cutting and pasting pleasure!  Note that you can use the advanced controls here to pop out to a new window or show plain code for copy and paste.<\/p>\n<pre class=\"lang:ps decode:true\">#.Synopsis\r\n#    This is a quick and open-ended script multi-threader searcher\r\n#    \r\n#.Description\r\n#    This script will allow any general, external script to be multithreaded by providing a single\r\n#    argument to that script and opening it in a seperate thread.  It works as a filter in the \r\n#    pipeline, or as a standalone script.  It will read the argument either from the pipeline\r\n#    or from a filename provided.  It will send the results of the child script down the pipeline,\r\n#    so it is best to use a script that returns some sort of object.\r\n#\r\n#    Authored by Ryan Witschger - http:\/\/www.Get-Blog.com\r\n#    \r\n#.PARAMETER Command\r\n#    This is where you provide the PowerShell Cmdlet \/ Script file that you want to multithread.  \r\n#    You can also choose a built in cmdlet.  Keep in mind that your script.  This script is read into \r\n#    a scriptblock, so any unforeseen errors are likely caused by the conversion to a script block.\r\n#    \r\n#.PARAMETER ObjectList\r\n#    The objectlist represents the arguments that are provided to the child script.  This is an open ended\r\n#    argument and can take a single object from the pipeline, an array, a collection, or a file name.  The \r\n#    multithreading script does it's best to find out which you have provided and handle it as such.  \r\n#    If you would like to provide a file, then the file is read with one object on each line and will \r\n#    be provided as is to the script you are running as a string.  If this is not desired, then use an array.\r\n#    \r\n#.PARAMETER InputParam\r\n#    This allows you to specify the parameter for which your input objects are to be evaluated.  As an example, \r\n#    if you were to provide a computer name to the Get-Process cmdlet as just an argument, it would attempt to \r\n#    find all processes where the name was the provided computername and fail.  You need to specify that the \r\n#    parameter that you are providing is the \"ComputerName\".\r\n#\r\n#.PARAMETER AddParam\r\n#    This allows you to specify additional parameters to the running command.  For instance, if you are trying\r\n#    to find the status of the \"BITS\" service on all servers in your list, you will need to specify the \"Name\"\r\n#    parameter.  This command takes a hash pair formatted as follows:  \r\n#\r\n#    @{\"ParameterName\" = \"Value\"}\r\n#    @{\"ParameterName\" = \"Value\" ; \"ParameterTwo\" = \"Value2\"}\r\n#\r\n#.PARAMETER AddSwitch\r\n#    This allows you to add additional switches to the command you are running.  For instance, you may want \r\n#    to include \"RequiredServices\" to the \"Get-Service\" cmdlet.  This parameter will take a single string, or \r\n#    an aray of strings as follows:\r\n#\r\n#    \"RequiredServices\"\r\n#    @(\"RequiredServices\", \"DependentServices\")\r\n#\r\n#.PARAMETER MaxThreads\r\n#    This is the maximum number of threads to run at any given time.  If resources are too congested try lowering\r\n#    this number.  The default value is 20.\r\n#    \r\n#.PARAMETER SleepTimer\r\n#    This is the time between cycles of the child process detection cycle.  The default value is 200ms.  If CPU \r\n#    utilization is high then you can consider increasing this delay.  If the child script takes a long time to\r\n#    run, then you might increase this value to around 1000 (or 1 second in the detection cycle).\r\n#\r\n#    \r\n#.EXAMPLE\r\n#    Both of these will execute the script named ServerInfo.ps1 and provide each of the server names in AllServers.txt\r\n#    while providing the results to the screen.  The results will be the output of the child script.\r\n#    \r\n#    gc AllServers.txt | .\\Run-CommandMultiThreaded.ps1 -Command .\\ServerInfo.ps1\r\n#    .\\Run-CommandMultiThreaded.ps1 -Command .\\ServerInfo.ps1 -ObjectList (gc .\\AllServers.txt)\r\n#\r\n#.EXAMPLE\r\n#    The following demonstrates the use of the AddParam statement\r\n#    \r\n#    $ObjectList | .\\Run-CommandMultiThreaded.ps1 -Command \"Get-Service\" -InputParam ComputerName -AddParam @{\"Name\" = \"BITS\"}\r\n#    \r\n#.EXAMPLE\r\n#    The following demonstrates the use of the AddSwitch statement\r\n#    \r\n#    $ObjectList | .\\Run-CommandMultiThreaded.ps1 -Command \"Get-Service\" -AddSwitch @(\"RequiredServices\", \"DependentServices\")\r\n#\r\n#.EXAMPLE\r\n#    The following demonstrates the use of the script in the pipeline\r\n#    \r\n#    $ObjectList | .\\Run-CommandMultiThreaded.ps1 -Command \"Get-Service\" -InputParam ComputerName -AddParam @{\"Name\" = \"BITS\"} | Select Status, MachineName\r\n#\r\n\r\n\r\nParam($Command = $(Read-Host \"Enter the script file\"), \r\n    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]$ObjectList,\r\n    $InputParam = $Null,\r\n    $MaxThreads = 20,\r\n    $SleepTimer = 200,\r\n    $MaxResultTime = 120,\r\n    [HashTable]$AddParam = @{},\r\n    [Array]$AddSwitch = @()\r\n)\r\n\r\nBegin{\r\n    $ISS = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()\r\n    $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads, $ISS, $Host)\r\n    $RunspacePool.Open()\r\n        \r\n    If ($(Get-Command | Select-Object Name) -match $Command){\r\n        $Code = $Null\r\n    }Else{\r\n        $OFS = \"`r`n\"\r\n        $Code = [ScriptBlock]::Create($(Get-Content $Command))\r\n        Remove-Variable OFS\r\n    }\r\n    $Jobs = @()\r\n}\r\n\r\nProcess{\r\n    Write-Progress -Activity \"Preloading threads\" -Status \"Starting Job $($jobs.count)\"\r\n    ForEach ($Object in $ObjectList){\r\n        If ($Code -eq $Null){\r\n            $PowershellThread = [powershell]::Create().AddCommand($Command)\r\n        }Else{\r\n            $PowershellThread = [powershell]::Create().AddScript($Code)\r\n        }\r\n        If ($InputParam -ne $Null){\r\n            $PowershellThread.AddParameter($InputParam, $Object.ToString()) | out-null\r\n        }Else{\r\n            $PowershellThread.AddArgument($Object.ToString()) | out-null\r\n        }\r\n        ForEach($Key in $AddParam.Keys){\r\n            $PowershellThread.AddParameter($Key, $AddParam.$key) | out-null\r\n        }\r\n        ForEach($Switch in $AddSwitch){\r\n            $Switch\r\n            $PowershellThread.AddParameter($Switch) | out-null\r\n        }\r\n        $PowershellThread.RunspacePool = $RunspacePool\r\n        $Handle = $PowershellThread.BeginInvoke()\r\n        $Job = \"\" | Select-Object Handle, Thread, object\r\n        $Job.Handle = $Handle\r\n        $Job.Thread = $PowershellThread\r\n        $Job.Object = $Object.ToString()\r\n        $Jobs += $Job\r\n    }\r\n        \r\n}\r\n\r\nEnd{\r\n    $ResultTimer = Get-Date\r\n    While (@($Jobs | Where-Object {$_.Handle -ne $Null}).count -gt 0)  {\r\n    \r\n        $Remaining = \"$($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).object)\"\r\n        If ($Remaining.Length -gt 60){\r\n            $Remaining = $Remaining.Substring(0,60) + \"...\"\r\n        }\r\n        Write-Progress `\r\n            -Activity \"Waiting for Jobs - $($MaxThreads - $($RunspacePool.GetAvailableRunspaces())) of $MaxThreads threads running\" `\r\n            -PercentComplete (($Jobs.count - $($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).count)) \/ $Jobs.Count * 100) `\r\n            -Status \"$(@($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False})).count) remaining - $remaining\" \r\n\r\n        ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})){\r\n            $Job.Thread.EndInvoke($Job.Handle)\r\n            $Job.Thread.Dispose()\r\n            $Job.Thread = $Null\r\n            $Job.Handle = $Null\r\n            $ResultTimer = Get-Date\r\n        }\r\n        If (($(Get-Date) - $ResultTimer).totalseconds -gt $MaxResultTime){\r\n            Write-Error \"Child script appears to be frozen, try increasing MaxResultTime\"\r\n            Exit\r\n        }\r\n        Start-Sleep -Milliseconds $SleepTimer\r\n        \r\n    } \r\n    $RunspacePool.Close() | Out-Null\r\n    $RunspacePool.Dispose() | Out-Null\r\n} <\/pre>\n<p>Further Reading:<br \/>\nInitial Session State:<br \/>\nhttp:\/\/msdn.microsoft.com\/en-us\/library\/system.management.automation.runspaces.initialsessionstate%28v=vs.85%29.aspx<br \/>\nRunspaces:<br \/>\nhttp:\/\/msdn.microsoft.com\/en-us\/library\/System.Management.Automation.Runspaces.Runspace(v=vs.85).aspx<br \/>\nPowerShell Class:<br \/>\nhttp:\/\/msdn.microsoft.com\/en-us\/library\/system.management.automation.powershell%28v=vs.85%29.aspx<br \/>\nPowerShell AddParameter Method:<br \/>\nhttp:\/\/msdn.microsoft.com\/en-us\/library\/system.management.automation.powershell.addparameter%28v=vs.85%29.aspx<\/p>\n","protected":false},"excerpt":{"rendered":"<p>As with all things, the longer you play with something the more you learn about it. It has been nearly 5 years since I wrote my original article on multithreading where I used PowerShell jobs to run multiple items at a time. In the conversations following that post I had a reader submit a fairly <a href='http:\/\/www.Get-Blog.com\/?p=189' class='excerpt-more'>[&#8230;]<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_mi_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[4],"tags":[8,9],"_links":{"self":[{"href":"http:\/\/www.Get-Blog.com\/index.php?rest_route=\/wp\/v2\/posts\/189"}],"collection":[{"href":"http:\/\/www.Get-Blog.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.Get-Blog.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.Get-Blog.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.Get-Blog.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=189"}],"version-history":[{"count":7,"href":"http:\/\/www.Get-Blog.com\/index.php?rest_route=\/wp\/v2\/posts\/189\/revisions"}],"predecessor-version":[{"id":199,"href":"http:\/\/www.Get-Blog.com\/index.php?rest_route=\/wp\/v2\/posts\/189\/revisions\/199"}],"wp:attachment":[{"href":"http:\/\/www.Get-Blog.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=189"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.Get-Blog.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=189"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.Get-Blog.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=189"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}