A Better (and Faster) Start-Job

by Nov 6, 2017

Start-Job transfers a script block to a new PowerShell process so that it can run separately and in parallel. Here is a very simple sample illustrating the idea behind jobs:

# three separate "jobs" to do:
$job1 = { Start-Sleep -Seconds 6  1 }
$job2 = { Start-Sleep -Seconds 8  2 }
$job3 = { Start-Sleep -Seconds 5  3 }

# execute two of them in background jobs
$j1 = Start-Job -ScriptBlock $job1
$j3 = Start-Job -ScriptBlock $job3

# execute one in our own process
$ej2 = & $job2

# wait for all to complete
$null = Wait-Job -Job $J1, $j3

# get the results and clean up
$ej1 = Receive-Job -Job $j1
$ej3 = Receive-Job -Job $j3
Remove-Job -Job $j1, $j3

# work with the results
$ej1, $ej2, $ej3

Rather than having to wait for 19 seconds, thanks to jobs it only takes around 8 seconds.

However, there are side effects: since jobs are executed in separate applications, data has to be transferred back and forth using XML serialization. The more data a job returns, the more time it takes, and sometimes this overhead overweighs the advantages.

A better way would be to run jobs in separate threads inside your own PowerShell instance. The below code allows just this. It adds a new command called “Start-MemoryJob” which can replace “Start-Job”. The remaining code stays exactly the same.

With Start-MemoryJob, there is no need anymore for object serialization. Your jobs run fast and smoothly, regardless of amount of data returned. Plus, you now receive the original objects. No longer do you have to deal with stripped serialization objects.

$code = @'
using System;
using System.Collections.Generic;
using System.Text;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
namespace InProcess
{
 public class InMemoryJob : System.Management.Automation.Job
 {
 public InMemoryJob(ScriptBlock scriptBlock, string name)
 {
 _PowerShell = PowerShell.Create().AddScript(scriptBlock.ToString());
 SetUpStreams(name);
 }
 public InMemoryJob(PowerShell PowerShell, string name)
 {
 _PowerShell = PowerShell;
 SetUpStreams(name);
 }
 private void SetUpStreams(string name)
 {
 _PowerShell.Streams.Verbose = this.Verbose;
 _PowerShell.Streams.Error = this.Error;
 _PowerShell.Streams.Debug = this.Debug;
 _PowerShell.Streams.Warning = this.Warning;
 _PowerShell.Runspace.AvailabilityChanged +=
 new EventHandler<RunspaceAvailabilityEventArgs>(Runspace_AvailabilityChanged);
 int id = System.Threading.Interlocked.Add(ref InMemoryJobNumber, 1);
 if (!string.IsNullOrEmpty(name))
 {
 this.Name = name;
 }
 else
 {
 this.Name = "InProcessJob" + id;
 }
 }
 void Runspace_AvailabilityChanged(object sender, RunspaceAvailabilityEventArgs e)
 {
 if (e.RunspaceAvailability == RunspaceAvailability.Available)
 {
 this.SetJobState(JobState.Completed);
 }
 }
 PowerShell _PowerShell;
 static int InMemoryJobNumber = 0;
 public override bool HasMoreData
 {
 get {
 return (Output.Count > 0);
 }
 }
 public override string Location
 {
 get { return "In Process"; }
 }
 public override string StatusMessage
 {
 get { return "A new status message"; }
 }
 protected override void Dispose(bool disposing)
 {
 if (disposing)
 {
 if (!isDisposed)
 {
 isDisposed = true;
 try
 {
 if (!IsFinishedState(JobStateInfo.State))
 {
 StopJob();
 }
 foreach (Job job in ChildJobs)
 {
 job.Dispose();
 }
 }
 finally
 {
 base.Dispose(disposing);
 }
 }
 }
 }
 private bool isDisposed = false;
 internal bool IsFinishedState(JobState state)
 {
 return (state == JobState.Completed || state == JobState.Failed || state ==
JobState.Stopped);
 }
 public override void StopJob()
 {
 _PowerShell.Stop();
 _PowerShell.EndInvoke(_asyncResult);
 SetJobState(JobState.Stopped);
 }
 public void Start()
 {
 _asyncResult = _PowerShell.BeginInvoke<PSObject, PSObject>(null, Output);
 SetJobState(JobState.Running);
 }
 IAsyncResult _asyncResult;
 public void WaitJob()
 {
 _asyncResult.AsyncWaitHandle.WaitOne();
 }
 public void WaitJob(TimeSpan timeout)
 {
 _asyncResult.AsyncWaitHandle.WaitOne(timeout);
 }
 }
}
'@
Add-Type -TypeDefinition $code
function Start-JobInProcess
{
  [CmdletBinding()]
  param
  (
    [scriptblock] $ScriptBlock,
    $ArgumentList,
    [string] $Name
  )
  function Get-JobRepository
  {
    [cmdletbinding()]
    param()
    $pscmdlet.JobRepository
  }
  function Add-Job
  {
    [cmdletbinding()]
    param
    (
      $job
    )
    $pscmdlet.JobRepository.Add($job)
  }
  if ($ArgumentList)
  {
    $PowerShell = [PowerShell]::Create().AddScript($ScriptBlock).AddArgument($argumentlist)
    $MemoryJob = New-Object InProcess.InMemoryJob $PowerShell, $Name
  }
  else
  {
    $MemoryJob = New-Object InProcess.InMemoryJob $ScriptBlock, $Name
  }
  $MemoryJob.Start()
  Add-Job $MemoryJob
  $MemoryJob
}

Twitter This Tip! ReTweet this Tip!