powershellpowershell-7.0powershell-7.2

Why is the function not called by Start-ThreadJob?


I want to backup some data over my script. All data shall be zipped in a separate thread. But two things go wrong:

  1. The function testFct is never called - indication: no "inside: ...".
  2. The parameter ZipSource is missing - see output.

Calling the script results in:

> .\Backup.ps1
outside: -What:  Data A   -ZipSource     -ZipDest  C:\Users\xyz\AppData\Local\Temp  -Timestamp "20220517-002854
outside: -What:  Data B   -ZipSource     -ZipDest  C:\Users\xyz\AppData\Local\Temp  -Timestamp "20220517-002854
>

Here is my Script:

class BackupContentData
{
    [ValidateNotNullOrEmpty()][string]$What
    [ValidateNotNullOrEmpty()][string]$ZipSource
}
$bcd = @(
    [BackupContentData]@{ What="Data A";   ZipSource="$env:USERPROFILE\Documents\a_file.txt";}
    [BackupContentData]@{ What="Data B";   ZipSource="$env:USERPROFILE\Documents\b_file.txt";}
)

function testFct {
    param([string]$What, [string]$ZipSource, [string]$ZipDest, [string]$Timestamp)
    
    Write-Host "inside: -What: "$What"  -ZipSource "$ZipSource"  -ZipDest "$ZipDest"  -Timestamp "$Timestamp
}


$timestamp="$(get-date -f yyyyMMdd-HHmmss)"

foreach ($e in $bcd) {
    $job = Start-ThreadJob  -Name $e.What  -InputObject $e  -ScriptBlock {
        Invoke-Expression "function getTest {$using:testFct}"
    
        Write-Host "outside: -What: "$input.What"  -ZipSource "$input.ZipSource"  -ZipDest "$env:Temp"  -Timestamp ""$(get-date -f yyyyMMdd-HHmmss)"
    
        getTest -What "$input.What"  -ZipSource "$input.ZipSource"  -ZipDest "$env:Temp"  -Timestamp "$(get-date -f yyyyMMdd-HHmmss)"
    }

    Receive-Job $job -AutoRemoveJob -Wait
}

What is wrong with the script?


Solution

  • Since testFct doesn't exist in the scope of your ThreadJob, you need to first store the function's definition and then pass it to the runspace scope and define the function there as demonstrated in this answer.

    The other issue is trying to reference the same $input more than one time. Due to the nature of the automatic variable $input, you can only reference this variable once in your script block:

    Since $input is an enumerator, accessing any of its properties causes $input to no longer be available. You can store $input in another variable to reuse the $input properties.

    As a workaround you could wrap the variable in the Array subexpression operator @( ) or the Subexpression operator $( ) to store the enumerated output in a new variable.

    Here is a simple example of what's explained above:

    Start-ThreadJob -InputObject 'Hello World' -ScriptBlock {
        "1. $input"
        "2. $input"
    } | Receive-Job -AutoRemoveJob -Wait
    
    # This outputs:
    # 1. Hello World
    # 2.
    
    # And the workaround
    Start-ThreadJob -InputObject 'Hello World' -ScriptBlock {
        # This would also work:
        # $thisInput = foreach($i in $input) { $i }
    
        $thisInput = $($input)
        "1. $thisInput"
        "2. $thisInput"
    } | Receive-Job -AutoRemoveJob -Wait
    
    # Outputs:
    # 1. Hello World
    # 2. Hello World
    

    Lastly, your script is not actually multi-threading, this is because you're storing the job inside your loop and then waiting for it sequentially instead of starting all jobs at once and then waiting for all of them.

    class BackupContentData {
        [ValidateNotNullOrEmpty()] [string] $What
        [ValidateNotNullOrEmpty()] [string] $ZipSource
    }
    function testFct {
        param([string]$What, [string]$ZipSource, [string]$ZipDest, [string]$Timestamp)
        Write-Host "inside: -What: $What -ZipSource $ZipSource -ZipDest $ZipDest -Timestamp $Timestamp"
    }
    
    # definition of the function is stored here
    $def = ${function:testFct}.ToString()
    
    $bcd = @(
        [BackupContentData]@{ What="Data A"; ZipSource="$env:USERPROFILE\Documents\a_file.txt" }
        [BackupContentData]@{ What="Data B"; ZipSource="$env:USERPROFILE\Documents\b_file.txt" }
    )
    
    $job = foreach ($e in $bcd) {
        Start-ThreadJob -Name $e.What -InputObject $e -ScriptBlock {
            $thisObject = $($input)
            # Define a new function with name `getTest` in this scope using `testFct` definition
            ${function:getTest} = $using:def
            Write-Host "outside: -What: $($thisObject.What) -ZipSource $($thisObject.ZipSource) -ZipDest $env:Temp -Timestamp $(get-date -f yyyyMMdd-HHmmss)"
            getTest -What $thisObject.What -ZipSource $thisObject.ZipSource -ZipDest $env:Temp -Timestamp (Get-Date -f yyyyMMdd-HHmmss)
        }
    }
    $job | Receive-Job -AutoRemoveJob -Wait
    

    The output you can expect from this:

    outside: -What: Data A -ZipSource C:\Users\user\Documents\a_file.txt -ZipDest C:\Users\user\AppData\Local\Temp -Timestamp 20220516-202251
    inside: -What: Data A -ZipSource C:\Users\user\Documents\a_file.txt -ZipDest C:\Users\user\AppData\Local\Temp -Timestamp 20220516-202251
    outside: -What: Data B -ZipSource C:\Users\user\Documents\b_file.txt -ZipDest C:\Users\user\AppData\Local\Temp -Timestamp 20220516-202251
    inside: -What: Data B -ZipSource C:\Users\user\Documents\b_file.txt -ZipDest C:\Users\user\AppData\Local\Temp -Timestamp 20220516-202251