PowerShell Tip: Execute a PowerShell script remotly with multiple threads

Here is a PowerShell script that contains a function that I use to remotely execute scripts on multiple machines at the same time.

The script was originally developed by Ryan Witschger @ www.get-blog.com, but that website doesn’t exist anymore so I’m publishing my modified version of it.

Invoke-Multithreader.ps1

Function Invoke-Multithreader {
    Param($Command = $(Read-Host "Enter the script file"), 
        [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]$ObjectList,
        $InputParam = $Null,
        $MaxThreads = 20,
        $SleepTimer = 200,
        $MaxResultTime = 120,
        [HashTable]$AddParam = @{},
        [Array]$AddSwitch = @()
    )

    Begin{
        $ISS = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
        $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads, $ISS, $Host)
        $RunspacePool.Open()
        $Jobs = @()
    }

    Process{
        Write-Progress -Activity "Preloading threads" -Status "Starting Job $($jobs.count)"
        ForEach ($Object in $ObjectList) { $PowershellThread = [powershell]::Create().AddCommand($Command) 
            If ($InputParam -ne $Null) { $PowershellThread.AddParameter($InputParam, $Object.ToString()) | out-null }
            Else { $PowershellThread.AddArgument($Object.ToString()) | out-null }
            ForEach($Key in $AddParam.Keys) { $PowershellThread.AddParameter($Key, $AddParam.$key) | out-null }
            ForEach($Switch in $AddSwitch) {
                $Switch
                $PowershellThread.AddParameter($Switch) | out-null
            }
            $PowershellThread.RunspacePool = $RunspacePool
            $Handle = $PowershellThread.BeginInvoke()
            $Job = "" | Select-Object Handle, Thread, object
            $Job.Handle = $Handle
            $Job.Thread = $PowershellThread
            $Job.Object = $Object.ToString()
            $Jobs += $Job
        }
    }

    End{
        $ResultTimer = Get-Date
        While (@($Jobs | Where-Object {$_.Handle -ne $Null}).count -gt 0)  {
    
            $Remaining = "$($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).object)"
            If ($Remaining.Length -gt 60){ $Remaining = $Remaining.Substring(0,60) + "..." }
            Write-Progress `
                -Activity "Waiting for Jobs - $($MaxThreads - $($RunspacePool.GetAvailableRunspaces())) of $MaxThreads threads running" `
                -PercentComplete (($Jobs.count - $($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).count)) / $Jobs.Count * 100) `
                -Status "$(@($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False})).count) remaining - $remaining" 

            ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})){
                $Job.Thread.EndInvoke($Job.Handle)
                $Job.Thread.Dispose()
                $Job.Thread = $Null
                $Job.Handle = $Null
                $ResultTimer = Get-Date
            }
            If (($(Get-Date) - $ResultTimer).totalseconds -gt $MaxResultTime){
                Write-Error "Child script appears to be frozen, try increasing MaxResultTime"
                Exit
            }
            Start-Sleep -Milliseconds $SleepTimer
        
        } 
        $RunspacePool.Close() | Out-Null
        $RunspacePool.Dispose() | Out-Null
    } 
}

Usage:

$Computers = “Server01”, “Server02”, “Server03”

Invoke-Multithreader -Command “script.ps1” -ObjectList $Computers -MaxThreads 4 -InputParam Computer -AddParam @{“Parameter1” = $Parameter1Value}

The -InputParam and -AddParam parameters are optional, but required if the script you try to execute have parameters you want to pass along.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Scroll to Top