Search code examples
powershellpowershell-5.1start-job

Question on use of Powershell start-job -scriptblock or sajb


I see many examples along the lines of:

Get-ChildItem -Filter "*.txt" | ForEach-Object { sajb {ren $_.fullname ($_.directoryname + "\" + "temp_" + $_.name + ".newext") } }

or what I think should be equivalent, using start-job -scriptblock:

Get-ChildItem -Filter "*.txt" | ForEach-Object { Start-Job -ScriptBlock {ren $_.fullname ($_.directoryname + "\" + "temp_" + $_.name + ".newext") } }

But these don't work for me. I get output like this and nothing happens to the files:

enter image description here

or this for my actual usecase:

enter image description here

If I remove the sajb block, then it works fine in series and does exactly what you'd expect. It's only when I try to run all commands in the loop in parallel that it fails.

The same operations do work fine from the Command prompt using:

for %x in ("*.txt") do (start "Convert" cmd /c "ren "%x" "temp_%x.newext"")

My purpose is to do this for some commands that run slowly in series but would run quickly in parallel using ffmpeg and sox, which also work fine in the for loop from the command prompt. I just can't get it working in PowerShell, even in the simple case like the file rename example above. What am I doing wrong with the start-job / sajb?

If it matters, I want to run this as a single PowerShell command from the PS prompt. I do not want to create a PowerShell script.

I see other posts with what look like similar questions, but I don't think they ever received functional answers, or if those answer are correct, I don't understand how to apply them to my situation:

Powershell start-job scriptblock not executing

Powershell Start-Job not executing scriptblock


Solution

    • sajb is simply a built-in alias of the Start-Job cmdlet.

    • Two asides:

      • The Start-ThreadJob cmdlet offers a lightweight, much faster thread-based alternative to the child-process-based regular background jobs created with Start-Job. It comes with PowerShell (Core) 7+ and in Windows PowerShell can be installed on demand with, e.g., Install-Module ThreadJob -Scope CurrentUser. In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.

      • In PowerShell (Core) 7+, the simplest solution is to use ForEach-Object with the -Parallel parameter, which combines parallel execution with direct access to pipeline input via the automatic $_ variable:

        1..3 |
          ForEach-Object -Parallel { "`$_ is: $_" }
        
    • As Santiago Squarzon notes, any Start-Job (as well as Start-ThreadJob) call inside a (non-parallel) ForEach-Object call will not automatically see the automatic $_ variable reflecting the current pipeline input object, given that it executes in a different runspace (in the case of Start-Job, a runspace in a different process); therefore, you must reference / pass its value explicitly:

      • Either: use $using:_, via the $using: scope:

         1..3 |
           ForEach-Object { Start-Job { $using:_ } } | 
           Receive-Job -Wait -AutoRemoveJob
        
        • Note: Unexpectedly, up to at least PowerShell 7.3.5 (current as of this writing), calling methods - as opposed to accessing properties - on $using: references requires enclosure in (...) - see GitHub issue #10876
      • Or: pass any values to the job via the -ArgumentList (-Args) parameter, which the job can then access via the automatic $args variable:

         1..3 |
           ForEach-Object { Start-Job { $args[0] } -ArgumentList $_ } | 
           Receive-Job -Wait -AutoRemoveJob
        
      • Additionally, note that in Windows PowerShell (the legacy PowerShell edition whose latest and last version is 5.1), Start-Job script blocks use a default working directory rather than inheriting the caller's - see the bottom section of this answer for details.