In a previous post, we covered Foreach-Object -Parallel. Now, we’re going a step further and deeper. We’re going to make the loop multi-threaded.

First, a bit of technical background.

Namespaces

System.Collections – includes ArrayList and Hashtable.

These classes work with a Synchronized property, which returns a thread-safe wrapper around the collection. The entire collection is locked (deadlock) for each Add or Remove action. Each action must wait its turn, preventing parallel execution of tasks. The collection has its own protection mechanism to ensure other threads do not corrupt the collection.

System.Collections.Generic – includes List<T> and Dictionary<TKey,TValue>.

These classes provide improved type safety and performance compared to System.Collections. However, they do not offer thread synchronization. This means that if we use them across multiple threads, the responsibility to ensure the collection does not become corrupted lies with us.

System.Collections.Concurrent

These classes also provide type safety and performance, similar to System.Collections.Generic, and are fully thread-safe. When working with multiple threads, it is highly recommended to use this namespace.

Classes in the Concurrent Namespace

Below is a selection of collections that fall under the concurrent namespace.

ConcurrentQueue<T>Represents a thread-safe first in-first out (FIFO) collection.
ConcurrentStack<T>Represents a thread-safe last in-first out (LIFO) collection.
ConcurrentBag<T>Represents a thread-safe, unordered collection of objects.
ConcurrentDictionary<TKey,TValue>Represents a thread-safe collection of key/value pairs that can be accessed by multiple threads concurrently.

Example

Let’s revisit the example of pinging IP addresses. We’re going to refactor the code to use a set number of threads that we create once and remove only at the end. This means we’ll be reusing the threads/runspaces.

First, we define the number of threads we want to use. For this example, we’ll use 5, which is the default for ThrottleLimit and what we used in other tests.

$Threads = 5

We create a ConcurrentQueue with the IP addresses we want to ping. We use a ConcurrentQueue because it is thread-safe. This allows us to add and remove items from multiple threads simultaneously without encountering issues. We fill this ConcurrentQueue with integers from 1 to 254, representing the last octets of the IP addresses we want to ping.

$FourthOctetList = [System.Collections.Concurrent.ConcurrentQueue[int]](1..254)

Next, we set up the ForEach-Object loop. We’ll use the -Parallel parameter to execute the code in parallel. We specify the -ThrottleLimit parameter to indicate how many threads we want to use simultaneously. We’ll populate the loop with the number of threads we’ve specified.

1 .. $Threads | ForEach-Object -ThrottleLimit $Threads -Parallel {}

Inside the loop, we’ll create the ConcurrentQueue that we can use within the loop. Since we saw in the previous post that in a ForEach-Object -Parallel we need to consider the scope, we use $Using:. We create an $Item to retrieve items from the ConcurrentQueue. We create a $ThreadNumber to show which thread is working. We create an $ActionsPerThread to count how many actions the thread has performed.

1 .. $Threads | ForEach-Object -ThrottleLimit $Threads -Parallel {
    $MultiThreadedList = $using:FourthOctetList
    $Item = $null
    $ThreadNumber = $_
    $ActionsPerThread = 0
}

We are going to check the ConcurrentQueue to see if there are any items left. If there are items, we remove them using the TryDequeue method. We create an IP address with the $Item that we retrieved from the ConcurrentQueue. We display which thread is working and which IP address is being pinged. We ping the IP address and show whether it is online or offline. We count the actions performed by the thread and clear the $Item.

while ($MultiThreadedList.count -gt 0) {
    if ($MultiThreadedList.TryDequeue([ref]$Item)) {

        $ip = "10.10.10.$Item"
        Write-Output "[$($ThreadNumber)] $ip"

        $result = Test-Connection -ComputerName $ip -Count 1 -Quiet
        if ($result) {
            Write-Output "$ip is online"
        } else {
            Write-Output "$ip is offline"
        }

        $ActionsPerThread++
        $item = $null
    }
}

Next, we can display how many actions were performed by the thread.

Write-Output "$ThreadNumber | $ActionsPerThread"

When we combine this, we get the following code:

$Threads = 5
$FourthOctetList = [System.Collections.Concurrent.ConcurrentQueue[int]](1..254)

1 .. $Threads | ForEach-Object -ThrottleLimit $Threads -Parallel {
    $MultiThreadedList = $using:FourthOctetList
    $Item = $null
    $ThreadNumber = $_
    $ActionsPerThread = 0

    while ($MultiThreadedList.count -gt 0) {
        if ($MultiThreadedList.TryDequeue([ref]$Item)) {

            $ip = "10.10.10.$Item"
            Write-Output "[$($ThreadNumber)] $ip"

            $result = Test-Connection -ComputerName $ip -Count 1 -Quiet
            if ($result) {
                Write-Output "$ip is online"
            } else {
                Write-Output "$ip is offline"
            }

            $ActionsPerThread++
            $item = $null
        }
    }

    Write-Output "$ThreadNumber | $ActionsPerThread"
}

And we see the following result:

[2] 192.168.1.2
[1] 192.168.1.1
[3] 192.168.1.3
[4] 192.168.1.4
[5] 192.168.1.5
192.168.1.1 is online
[1] 192.168.1.6
192.168.1.5 is online
[5] 192.168.1.7
192.168.1.4 is online
[4] 192.168.1.8
192.168.1.8 is online
[4] 192.168.1.9
192.168.1.9 is online

&lt;Skipping some output>

192.168.1.247 is offline
[5] 192.168.1.253
[3] 192.168.1.254
1 | 53
192.168.1.250 is offline
4 | 53
192.168.1.254 is offline
192.168.1.253 is offline
5 | 49
3 | 51
192.168.1.252 is offline
2 | 48

And if we use Measure-Command to check the duration, we see that this method is even faster than ForEach-Object -Parallel, as we observed in the previous post. This is because we reuse the threads instead of creating and removing them each time.

$Threads = 5
$FourthOctetList = [System.Collections.Concurrent.ConcurrentQueue[int]](1..254)

Measure-Command { 
    1 .. $Threads | ForEach-Object -ThrottleLimit $Threads -Parallel {
        $MultiThreadedList = $using:FourthOctetList
        $Item = $null
        $ThreadNumber = $_
        $ActionsPerThread = 0

        while ($MultiThreadedList.count -gt 0) {
            if ($MultiThreadedList.TryDequeue([ref]$Item)) {

                $ip = "10.10.10.$Item"
                Write-Output "[$($ThreadNumber)] $ip"

                $result = Test-Connection -ComputerName $ip -Count 1 -Quiet
                if ($result) {
                    Write-Output "$ip is online"
                } else {
                    Write-Output "$ip is offline"
                }

                $ActionsPerThread++
                $item = $null
            }
        }

        Write-Output "$ThreadNumber | $ActionsPerThread"
    }
}

Minutes           : 1
Seconds           : 10
Milliseconds      : 216
TotalMilliseconds : 70216.8207

Summary

For me, this method is also relatively new, and I am still learning what it can do for me. However, since I enjoy exploring and mastering this, I wanted to share the above with you. I have already used this several times to perform relatively light but time-intensive tasks more quickly.

I hope you find this useful and am certainly open to improvements and additions.

Leave a Reply

Your email address will not be published. Required fields are marked *