My most popular article has thus far been the post over multithreading found here. As such I felt that my audience might like to see some more entries covering that topic. After all, it is a vast topic with a lot of very advanced things you can do. In this post I would like to talk about tracking the status of your various threads. In other words, how can you see a progress of what your child script is doing?
In exploring this myself, I worked with passing custom objects back to the parent which held various status messages that the parent could read in with the “receive-job” command. That actually worked, but it took a lot of code in both the parent and the child to generate and read the status from the child threads.
While working with this, though, I found that the Job object from “get-job”, a System.Management.Automation.Job, has a property called “Progress”. I then decided to look into how this actually gets filled out. And the answer eluded my research until I managed to do it, completely by accident. As it turns out, this very useful property is filled in automatically by a “write-progress” command run within the child thread. It is actually quite logical that the progress field is filled in by “write-progress”.
So, that is enough of a background on how I came about it. Let’s actually look at some code. First,I want to create a script block to run within my child job.
$ThreadCode = {
$x = 0
$time = 200
While ($x -lt 100){
$x++
Write-Progress -Activity "Testing"
-Status "Testing"
-PercentComplete $X
-completed
Start-Sleep -Milliseconds $time
}
Write-Progress -Activity "Testing"
-Status "Testing"
-PercentComplete 100
-Completed
"Annnd we're done."
}
So here you’ll notice that I am basically creating a loop that will count 1 to 100 and then close. Along the way it will fill out its progress with the “write-progress” cmdlet. The one important note is that I am adding the “-completed” parameter to the “write-progress” cmdlet. That may look like a mistake in the code,but it isn’t. If you play around with this manner of getting progress from a child you will notice that when you run a “receive-job” your host thread will read all output from the child, including flashing all of the “write-progress” bars that were in the child. If you actually read the help files for “write-progress”, you’ll see that actual description of the functionality of “-complete” states that is simply hides the output bar. We need to do this so that when the parent thread runs a “receive-job” there is no odd behavior for the end user.
Get-Job | Stop-Job
Get-Job | Remove-Job
## Start the child job and assign to a variable to save some code later
$Job = Start-Job -ScriptBlock $ThreadCode
So for this quick snippet we are just closing an remove any jobs our current session may have, and then starting our job. I assign it to a variable so we don’t have to track it down later, not that it’s hard.
## Get the Child job object
$Child = $Job.ChildJobs[0]
## Get the newest Progress Object in the child job
$Progress = $Child.Progress[$Child.Progress.Count - 1]
## If the progress object is not null
If ($Progress.Activity -ne $Null){
Write-Progress -Activity $Job.Name
-Status $Progress.StatusDescription
-PercentComplete $Progress.PercentComplete
-ID $Job.ID
}
Start-Sleep -Milliseconds 200
}
So this is the bread and butter of the parent script. It simply loops around as long as our job is running. First we need to find the child job to our parent job. I am not sure why Microsoft chose to design it like this, but each job you start actually runs commands in a child job. I have a feeling it has something to do with functionality with Windows HPC, but that is neither here nor there. Once we have our child job, we want to find the latest progress object that has been passed. After that I just check to see if the object is empty, because the first check may not have given the child thread enough time to fill this out. If there is data, then we can fill out a “write-progress” bar on our host thread. From there we just repeat until the job is done.
At the end we just want to read anything the child script may actually have sent. So that covers it. It is really easy to get custom progress bars for a child process because there is a good, built in method to do it.
Following is a full example of a script which will launch three child threads at a time until there have been 10 opened total. It will show you the output of each thread as it completes and the progress bars produced by each. I even included a random speed that each child will complete so you can see some bells and whistles going.
$ThreadCode = {
$x = 0
$time = Get-Random -Maximum 200 -SetSeed $(Get-Date).Millisecond
While ($x -lt 100){
$x++
Write-Progress -Activity "Testing" -Status "Testing" -PercentComplete $X -completed
Start-Sleep -Milliseconds $time
}
Write-Progress -Activity "Testing" -Status "Testing" -PercentComplete 100 -Completed
"Annnd we're done."
}
## Kill any current jobs in the sessions and get rid of them
Get-Job | Stop-Job
Get-Job | Remove-Job
## Loop control
$x = 0
## While we are still launching threads or wait for them to close
While (($x -lt 10) -or ($(Get-Job -State "Running").count -gt 0)){
## While there are less than three jobs running and less than 10 have started
While (($(Get-Job -State "Running").count -lt 3) -and ($x -lt 10)){
Start-Job -ScriptBlock $ThreadCode | Out-Null
$x++
}
## Main portion - Read all jobs
ForEach ($Job in Get-Job) {
## Read all children of all jobs
ForEach ($Child in $Job.ChildJobs){
## Get the latest progress object of the job
$Progress = $Child.Progress[$Child.Progress.Count - 1]
## If there is a progress object returned write progress
If ($Progress.Activity -ne $Null){
Write-Progress -Activity $Job.Name
-Status $Progress.StatusDescription
-PercentComplete $Progress.PercentComplete
-ID $Job.ID
}
## If this child is complete then stop writing progress
If ($Progress.PercentComplete -eq 100){
Write-Progress -Activity $Job.Name
-Status $Progress.StatusDescription
-PercentComplete $Progress.PercentComplete
-ID $Job.ID
-Complete
## Clear all progress entries so we don't process it again
$Child.Progress.Clear()
}
}
}
Get-Job -State "Completed" | Receive-Job
## Setting for loop processing speed
Start-Sleep -Milliseconds 200
}
Get-Job | Wait-Job | Out-Null
Get-Job | Receive-Job