3

I have a for loop that iterates through an ArrayList and during the process, adds more items to the list and processes them as well (iteratively). I am trying to convert this function to run concurrently using Runspacepool.

Here is the normal code without runspace:

$array = [System.Collections.ArrayList]@(1, 2, 3, 4, 5)
Write-Host "Number of items in array before loop: $($array.Count)"
for ($i = 0; $i -lt $array.Count; $i++) {
    Write-Host "Counter: $i`tArray: $array"
    if ($array[$i] -in @(1, 2, 3, 4, 5)) {
        $array.Add($array[$i] + 3) | Out-Null
    }
}
Write-Host "Array: $array"
Write-Host "Number of items in array after loop: $($array.Count)"

Output is:

Number of items in array before loop: 5
Counter: 0      Array: 1 2 3 4 5
Counter: 1      Array: 1 2 3 4 5 4
Counter: 2      Array: 1 2 3 4 5 4 5
Counter: 3      Array: 1 2 3 4 5 4 5 6
Counter: 4      Array: 1 2 3 4 5 4 5 6 7
Counter: 5      Array: 1 2 3 4 5 4 5 6 7 8
Counter: 6      Array: 1 2 3 4 5 4 5 6 7 8 7
Counter: 7      Array: 1 2 3 4 5 4 5 6 7 8 7 8
Counter: 8      Array: 1 2 3 4 5 4 5 6 7 8 7 8
Counter: 9      Array: 1 2 3 4 5 4 5 6 7 8 7 8
Counter: 10     Array: 1 2 3 4 5 4 5 6 7 8 7 8
Counter: 11     Array: 1 2 3 4 5 4 5 6 7 8 7 8
Array: 1 2 3 4 5 4 5 6 7 8 7 8
Number of items in array after loop: 12

Here is the Runspace function that I am trying to implement:

$pool = [RunspaceFactory]::CreateRunspacePool(1, 10)
$pool.Open()
$runspaces = @()

$scriptblock = {
    Param ($i, $array)
    # Start-Sleep 1 # <------ Output varies significantly if this is enabled
    Write-Output "$i value: $array"
    if ($i -in @(1, 2, 3, 4, 5)) {
        $array.Add($i + 3) | Out-Null
    }
}

$array = [System.Collections.ArrayList]::Synchronized(([System.Collections.ArrayList]$(1, 2, 3, 4, 5)))
Write-Host "Number of items in array before loop: $($array.Count)"
for ($i = 0; $i -lt $array.Count; $i++) {
    $runspace = [PowerShell]::Create().AddScript($scriptblock).AddArgument($array[$i]).AddArgument($array)
    $runspace.RunspacePool = $pool
    $runspaces += [PSCustomObject]@{ Pipe = $runspace; Status = $runspace.BeginInvoke() }
}

while ($runspaces.Status -ne $null) {
    $completed = $runspaces | Where-Object { $_.Status.IsCompleted -eq $true }
    foreach ($runspace in $completed) {
        $runspace.Pipe.EndInvoke($runspace.Status)
        $runspace.Status = $null
    }
}
Write-Host "array: $array"
Write-Host "Number of items in array after loop: $($array.Count)"
$pool.Close()
$pool.Dispose()

Output without sleep function is as expected:

Number of items in array before loop: 5
Current value: 1        Array: 1 2 3 4 5
Current value: 2        Array: 1 2 3 4 5 4
Current value: 3        Array: 1 2 3 4 5 4 5
Current value: 4        Array: 1 2 3 4 5 4 5 6
Current value: 5        Array: 1 2 3 4 5 4 5 6 7
Current value: 4        Array: 1 2 3 4 5 4 5 6 7 8
Current value: 5        Array: 1 2 3 4 5 4 5 6 7 8 7
Current value: 6        Array: 1 2 3 4 5 4 5 6 7 8 7
Current value: 7        Array: 1 2 3 4 5 4 5 6 7 8 7
Current value: 8        Array: 1 2 3 4 5 4 5 6 7 8 7
Current value: 7        Array: 1 2 3 4 5 4 5 6 7 8 7 8
Current value: 8        Array: 1 2 3 4 5 4 5 6 7 8 7 8
Array: 1 2 3 4 5 4 5 6 7 8 7 8
Number of items in array after loop: 12

Output with Sleep:

Number of items in array before loop: 5
Current value: 1        Array: 1 2 3 4 5
Current value: 2        Array: 1 2 3 4 5 4
Current value: 3        Array: 1 2 3 4 5 4 5
Current value: 4        Array: 1 2 3 4 5 4 5 6
Current value: 5        Array: 1 2 3 4 5 4 5 6 7
Array: 1 2 3 4 5 4 5 6 7 8
Number of items in array after loop: 10

I understand that this is happening because the for loop exits before the sleep time is completed and therefore, only the first 5 items are added to the runspace pool.

Is there a way to add more items to the ArrayList dynamically and still process them concurrently using runspaces?

Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
Suraj
  • 468
  • 1
  • 4
  • 14
  • 3
    Note that ```ArrayList``` is not thread-safe, so adding items in parallel runspaces is potentially going to have other issues as well, even if you fix this one. See https://learn.microsoft.com/en-us/dotnet/api/system.collections.arraylist?view=net-6.0#thread-safety – mclayton Jun 23 '22 at 08:05
  • 1
    I have used a `Synchronized ArrayList` here. Wouldn't it help? https://learn.microsoft.com/en-us/dotnet/api/system.collections.arraylist.synchronized?view=net-6.0#system-collections-arraylist-synchronized(system-collections-arraylist) – Suraj Jun 23 '22 at 10:38
  • 1
    The `for` loop ends before the runspaces can add to the array. The condition for ending your `for` loop is `$i -lt $array.Count`. `Start-Sleep` ensures that the runspaces have no time to add into that array to make it iterate further – Santiago Squarzon Jun 23 '22 at 13:24
  • 1
    Yes, I'm using `Start-Sleep` here to simulate a long running task that I have in my actual code. Same thing happens there too! Before the worker thread is able to process and get new items to add, the `for` loop exits. Can you think of a way to dynamically add into the `ArrayList` or iterate this? Is it possible to implement a recursive function using multithreading (Runspace pool)? – Suraj Jun 23 '22 at 16:47
  • 2
    You’re going to need to combine your ```for``` and ```while``` loops into a single loop that ((ii) starts new runspaces for items that have been added to ```$array``` since the last iteration and (ii) only exits when all runspaces have completed. I can’t rewrite that right now, but I’ll look later if no-one else gets there first… – mclayton Jun 23 '22 at 16:56
  • 1
    @mclayton has given a great hint in his comment. I would personally use a `ConcurrentQueue` and a `while` loop instead of a `for` loop. But I would need to understand what are you trying to do to understand how to approach the code. What you have right now in your question is a racing condition between your runspaces and your loop but that doesn't tell me why you want to do what you are doing – Santiago Squarzon Jun 23 '22 at 18:59
  • 1
    @Santiago, The actual problem involves `Invoke-RestMethod (irm)` towards multiple endpoints, based on the results of which, I may have to query more similar endpoints. Hence my analogy with the simple `ArrayList`. The actual `ArrayList` in my code is a collection of PSCustomObjects that contain the `GET` parameters needed for the `irm`. This way, I can query many REST endpoints concurrently (as individual web requests are time-consuming). – Suraj Jun 24 '22 at 02:50
  • 1
    @Suraj yeah that's exactly what a `ConcurrentQueue` is for – Santiago Squarzon Jun 24 '22 at 03:02

3 Answers3

2

The core of your "working" behaviour is that PowerShell was running your "non-sleep" scriptblocks faster than it could create them in the for loop, so the loop was seeing the new items being added by previous iterations before it reached the end of the array. As a result it had to process all of the items before it exited and moved on to the while loop.

When you added a Start-Sleep it shifted the balance, and it took longer to run the scriptblocks than it did to create them, so the for loop reached the end of the array before the new items were added by the earliest iterations.

The following script fixes this by combining your for and while loops to repeatedly alternate between (i) creating new threads and (ii) checking if they've finished, and only exiting when all the work is done.

However multi-threading is hard so it's best to assume I've made mistakes somewhere, and test properly before you release it to your live workflow...

$scriptblock = {
    Param ($i, $array)
    # random sleep to simulate variable-length workloads. this is
    # more likely to flush out error conditions than a fixed sleep 
    # period as threads will finish out-of-turn more often
    Start-Sleep (Get-Random -Minimum 1 -Maximum 10)
    Write-Output "$i value: $array"
    if ($i -in @(1, 2, 3, 4, 5)) {
        $array.Add($i + 3) | Out-Null
    }
}

$pool = [RunspaceFactory]::CreateRunspacePool(1, 10)
$pool.Open()

# note - your "$runspaces" variable is misleading as you're creating 
# "PowerShell" objects, and a "Runspace" is a different thing entirely,
# so I've called it $instances instead
# see https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell?view=powershellsdk-7.0.0
#  vs https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.runspace?view=powershellsdk-7.0.0
$instances = @()

$array = [System.Collections.ArrayList]::Synchronized(([System.Collections.ArrayList]$(1, 2, 3, 4, 5)))
Write-Host "Number of items in array before loop: $($array.Count)"

while( $true )
{

    # start PowerShell instances for any items in $array that don't already have one.
    # on the first iteration this will seed the initial instances, and in
    # subsequent iterations it will create new instances for items added to
    # $array since the last iteration.
    while( $instances.Length -lt $array.Count )
    {
        $instance = [PowerShell]::Create().AddScript($scriptblock).AddArgument($array[$instances.Length]).AddArgument($array);
        $instance.RunspacePool = $pool
        $instances += [PSCustomObject]@{ Value = $instance; Status = $instance.BeginInvoke() }
    }

    # watch out because there's a race condition here. it'll need very unlucky 
    # timing, *but* an instance might have added an item to $array just after
    # the while loop finished, but before the next line runs, so there *could* 
    # be an item in $array that hasn't had an instance created for it even
    # if all the current instances have completed

    # is there any more work to do? (try to mitigate the race condition
    # by checking again for any items in $array that don't have an instance
    # created for them)
    $active = @( $instances | Where-Object { -not $_.Status.IsCompleted } )
    if( ($active.Length -eq 0) -and ($instances.Length -eq $array.Count) )
    {
        # instances have been created for every item in $array,
        # *and* they've run to completion, so there's no more work to do
        break;
    }

    # if there are incomplete instances, wait for a short time to let them run
    # (this is to avoid a "busy wait" - https://en.wikipedia.org/wiki/Busy_waiting)
    Start-Sleep -Milliseconds 250;

}

# all the instances have completed, so end them
foreach ($instance in $instances)
{
    $instance.Value.EndInvoke($instance.Status);
}

Write-Host "array: $array"
Write-Host "Number of items in array after loop: $($array.Count)"
$pool.Close()
$pool.Dispose()

Example output:

Number of items in array before loop: 5
1 value: 1 2 3 4 5 6 5 7
2 value: 1 2 3 4 5 6
3 value: 1 2 3 4 5
4 value: 1 2 3 4 5 6 5
5 value: 1 2 3 4 5 6 5 7 4
6 value: 1 2 3 4 5 6 5 7
5 value: 1 2 3 4 5 6 5 7 4 8
7 value: 1 2 3 4 5 6 5 7
4 value: 1 2 3 4 5 6 5 7 4 8 8
8 value: 1 2 3 4 5 6 5 7 4 8 8
8 value: 1 2 3 4 5 6 5 7 4 8 8
7 value: 1 2 3 4 5 6 5 7 4 8 8 7

Note the order of items in the array will vary depending on the length of the random sleeps in the $scriptblock.

There are probably additional improvements that could be made, but this at least seems to work...

mclayton
  • 8,025
  • 2
  • 21
  • 26
  • seems like not many know this but, there is no need to wait for the async result to finish. `EndInvoke(...)` already does the waiting for us – Santiago Squarzon Jun 23 '22 at 21:13
  • 1
    @SantiagoSquarzon - tbh, I can't remember the last time (if ever) that I've actually used runspaces / background jobs / any sort of parallelism in production scripts, so my answer is based purely on trial and error just now - I'm definitely open to any suggestions for improvements... :-). I think one tricky thing here though is that a blocking wait on "Thread A" finishing might delay starting a scriptblock for a newly arrived item in the array that was created by "Thread B" in the meantime? We might be talking millisecond delays though, so might not really be an issue... – mclayton Jun 23 '22 at 21:37
1

This answer attempts to provide a better solution to the producer-consumer problem using a BlockingCollection<T> which provides an implementation of the producer/consumer pattern.

To clarify on the issue with my previous answer, as OP has noted in a comment:

If the starting count of the queue (say, 2) is less than the max number of threads (say 5), then only that many (2, in this case) threads remain active no matter how many ever items are added to the queue later. Only the starting number of threads process the rest of the items in the queue. In my case, the starting count is usually one. Then I make a irm (alias for Invoke-RestMethod) request, and add some 10~20 items. These are processed by only the first thread. The other threads go to Completed state right at the start. Is there a solution to this?

For this example, the runspaces will be using the TryTake(T, TimeSpan) method overload which blocks the thread and waits for the specified timeout. On each loop iteration the runspaces will also be updating a Synchronized Hashtable with their TryTake(..) result.

The main thread will be using the Synchronized Hashtable to wait until all runspaces had sent a $false status, when this happens an exit signal is sent to the threads to with .CompleteAdding().

Even though not perfect, this solves the problem where some of the threads might exit early from the loop and attempts to ensure that all threads end at the same time (when there are no more items in the collection).

The producer logic will be very similar to the previous answer, however, in this case each thread will wait random amount of time between $timeout.Seconds - 5 and $timeout.Seconds + 5 on each loop iteration.

The results one can expect from this demo can be found on this gist.

using namespace System.Management.Automation.Runspaces
using namespace System.Collections.Concurrent
using namespace System.Threading

try {
    $threads = 20
    $bc      = [BlockingCollection[int]]::new()
    $status  = [hashtable]::Synchronized(@{ TotalCount = 0 })

    # set a timer, all threads will wait for it before exiting
    # this timespan should be tweaked depending on the task at hand
    $timeout = [timespan]::FromSeconds(5)

    foreach($i in 1, 2, 3, 4, 5) {
        $bc.Add($i)
    }


    $scriptblock = {
        param([timespan] $timeout, [int] $threads)

        $id = [runspace]::DefaultRunspace
        $status[$id.InstanceId] = $true
        $syncRoot = $status.SyncRoot
        $release  = {
            [Threading.Monitor]::Exit($syncRoot)
            [Threading.Monitor]::PulseAll($syncRoot)
        }

        # will use this to simulate random delays
        $min = $timeout.Seconds - 5
        $max = $timeout.Seconds + 5

        [ref] $target = $null
        while(-not $bc.IsCompleted) {
            # NOTE from `Hashtable.Synchronized(Hashtable)` MS Docs:
            #
            #    The Synchronized method is thread safe for multiple readers and writers.
            #    Furthermore, the synchronized wrapper ensures that there is only
            #    one writer writing at a time.
            #
            #    Enumerating through a collection is intrinsically not a
            #    thread-safe procedure. Even when a collection is synchronized,
            #    other threads can still modify the collection, which causes the
            #    enumerator to throw an exception.

            # Mainly doing this (lock on the sync hash) to get the Active Count
            # Not really needed and only for demo porpuses

            # if we can't lock on this object in 200ms go next iteration
            if(-not [Threading.Monitor]::TryEnter($syncRoot, 200)) {
                continue
            }

            # if there are no items in queue, send `$false` to the main thread
            if(-not ($status[$id.InstanceId] = $bc.TryTake($target, $timeout))) {
                # release the lock and signal the threads they can get a handle
                & $release
                # and go next iteration
                continue
            }

            # if there was an item in queue, get the active count
            $active = @($status.Values -eq $true).Count
            # add 1 to the total count
            $status['TotalCount'] += 1
            # and release the lock
            & $release

            Write-Host (
                ('Target Value: {0}' -f $target.Value).PadRight(20) + '|'.PadRight(5) +
                ('Items in Queue: {0}' -f $bc.Count).PadRight(20)   + '|'.PadRight(5) +
                ('Runspace Id: {0}' -f $id.Id).PadRight(20)         + '|'.PadRight(5) +
                ('Active Runspaces [{0:D2} / {1:D2}]' -f $active, $threads)
            )

            $ran = [random]::new()
            # start a simulated delay
            Start-Sleep $ran.Next($min, $max)

            # get a random number between 0 and 10
            $ran = $ran.Next(11)
            # if the number is greater than the Dequeued Item
            if ($ran -gt $target.Value) {
                # enumerate starting from `$ran - 2` up to `$ran`
                foreach($i in ($ran - 2)..$ran) {
                    # enqueue each item
                    $bc.Add($i)
                }
            }

            # Send 1 to the Success Stream, this will help us check
            # if the test succeeded later on
            1
        }
    }

    $iss    = [initialsessionstate]::CreateDefault2()
    $rspool = [runspacefactory]::CreateRunspacePool(1, $threads, $iss, $Host)
    $rspool.ApartmentState = [ApartmentState]::STA
    $rspool.ThreadOptions  = [PSThreadOptions]::UseNewThread
    $rspool.InitialSessionState.Variables.Add([SessionStateVariableEntry[]]@(
        [SessionStateVariableEntry]::new('bc', $bc, 'Producer Consumer Collection')
        [SessionStateVariableEntry]::new('status', $status, 'Monitoring hash for signaling `.CompleteAdding()`')
    ))
    $rspool.Open()

    $params = @{
        Timeout = $timeout
        Threads = $threads
    }

    $rs = for($i = 0; $i -lt $threads; $i++) {
        $ps = [powershell]::Create($iss).AddScript($scriptblock).AddParameters($params)
        $ps.RunspacePool = $rspool

        @{
            Instance    = $ps
            AsyncResult = $ps.BeginInvoke()
        }
    }

    while($status.ContainsValue($true)) {
        Start-Sleep -Milliseconds 200
    }

    # send signal to stop
    $bc.CompleteAdding()

    [int[]] $totalCount = foreach($r in $rs) {
        try {
            $r.Instance.EndInvoke($r.AsyncResult)
            $r.Instance.Dispose()
        }
        catch {
            Write-Error $_
        }
    }
    Write-Host ("`nTotal Count [ IN {0} / OUT {1} ]" -f $totalCount.Count, $status['TotalCount'])
    Write-Host ("Items in Queue: {0}" -f $bc.Count)
    Write-Host ("Test Succeeded: {0}" -f (
        [Linq.Enumerable]::Sum($totalCount) -eq $status['TotalCount'] -and
        $bc.Count -eq 0
    ))
}
finally {
    ($bc, $rspool).ForEach('Dispose')
}
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • _A producing thread can call the `CompleteAdding` method to indicate that no more items will be added. Consumers monitor the `IsCompleted` property to know when the collection is empty and no more items will be added_. Instead of using a timeout, we could wait until the last thread is active and the queue is empty to use the `CompleteAdding` method? @mclayton's answer seems to doing that and it is working smoothly in my case. Ref: https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.blockingcollection-1?view=net-6.0 – Suraj Jun 26 '22 at 05:13
  • 1
    @Suraj `CompleteAdding` doesn't apply for this case because all threads are producers and consumers. How does one thread knows when to signal `CompleteAdding()` ? The example from the Docs you're looking at is for 1 Producer and multiple Consumers, different from this example. However I encourage you to accept mclayton's answer if it's the best at solving your problem. – Santiago Squarzon Jun 26 '22 at 05:20
  • 1
    Yep! Can't think of a way to use that method from within any of the runspace threads. We need the main thread to monitor the queue and decide. TYSM for the answers! – Suraj Jun 26 '22 at 05:43
  • 1
    I have an idea. Please correct me if I'm wrong here. Could we use a Synchronized stack that every thread can push into when they get into the `while(TryTake())` loop and pop when they get out. The main thread could monitor the stack in a while loop to check active threads and when the stack is empty, it would mean that there are no active threads and that the queue is empty Then the main thread could use the `CompleteAdding` method to stop all the threads? – Suraj Jun 26 '22 at 06:00
  • @Suraj I liked the idea, I have updated my answer but using a sync hashtable instead of a queue – Santiago Squarzon Jun 26 '22 at 07:19
  • Now that we are using `$status` to monitor the queue and the threads, can't we use `TryTake(T)` method instead of `TryTake(T, TimeSpan)`? Is the `$timeout` still required? – Suraj Jun 26 '22 at 07:45
  • 1
    @Suraj looping without blocking the threads will have a big hit on your CPU for no particular reason. You can do it if you like, I wouldn't. Besides, I don't see how waiting 30 seconds when there are no more items in queue can hurt. – Santiago Squarzon Jun 26 '22 at 07:48
  • @Suraj I've added some updates mainly for the Demo, you might want to look at it – Santiago Squarzon Jun 26 '22 at 15:13
  • 1
    Although you're locking `$status` in the runspace threads, the main thread still enumerates it here without locking: `while($status.ContainsValue($true))` :) – Suraj Jun 26 '22 at 15:21
  • 1
    @Suraj doesn't seem to be needed since the logic in the threads are already asking to lock on the object and whenever they can't the just go to the iteration. I ran many tests and all end up as expected but you can add the same logic of locking on and releasing in your code if you wish to. I did improve the logic a little bit and added more inline comments. Also reduced the timers to make it go more smoothly – Santiago Squarzon Jun 26 '22 at 19:20
0

Note, this answer DOES NOT provide a good solution to OP's problem. See this answer for a better take on the producer-consumer problem.


This is a different approach from mclayton's helpful answer, hopefully both answers can lead you to solve your problem. This example uses a ConcurrentQueue<T> and consists in multiple threads performing the same action.

As you may see, in this case we start only 5 threads that will be trying to dequeue the items concurrently.

If the randomly generated number between 0 and 10 is greater than the dequeued item, it creates an array starting from the random number - 2 up to the given random number and enqueues them (tries to simulate, badly, what you have posted in comments, "The actual problem involves Invoke-RestMethod (irm) towards multiple endpoints, based on the results of which, I may have to query more similar endpoints").

Do note, for this example I'm using $threads = $queue.Count, however this should not be always the case. Don't start too many threads or you might kill your session! Also be aware you might overload your network if querying multiple endpoints at the same time. I would say, keep the threads always below $queue.Count.

The results you can expect from below code should vary greatly on each runtime.

using namespace System.Management.Automation.Runspaces
using namespace System.Collections.Concurrent

try {
    $queue = [ConcurrentQueue[int]]::new()
    foreach($i in 1, 2, 3, 4, 5) {
        $queue.Enqueue($i)
    }
    $threads = $queue.Count

    $scriptblock = {
        [ref] $target = $null
        while($queue.TryDequeue($target)) {
            [pscustomobject]@{
                'Target Value'      = $target.Value
                'Elements in Queue' = $queue.Count
            }

            # get a random number between 0 and 10
            $ran = Get-Random -Maximum 11
            # if the number is greater than the Dequeued Item
            if ($ran -gt $target.Value) {
                # enumerate starting from `$ran - 2` up to `$ran`
                foreach($i in ($ran - 2)..$ran) {
                    # enqueue each item
                    $queue.Enqueue($i)
                }
            }
        }
    }

    $iss    = [initialsessionstate]::CreateDefault2()
    $rspool = [runspacefactory]::CreateRunspacePool(1, $threads, $iss, $Host)
    $rspool.InitialSessionState.Variables.Add([SessionStateVariableEntry]::new(
        'queue', $queue, ''
    ))
    $rspool.Open()

    $rs = for($i = 0; $i -lt $threads; $i++) {
        $ps = [powershell]::Create().AddScript($scriptblock)
        $ps.RunspacePool = $rspool

        @{
            Instance    = $ps
            AsyncResult = $ps.BeginInvoke()
        }
    }

    foreach($r in $rs) {
        try {
            $r.Instance.EndInvoke($r.AsyncResult)
            $r.Instance.Dispose()
        }
        catch {
            Write-Error $_
        }
    }
}
finally {
    $rspool.ForEach('Dispose')
}
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    Woah!! Is that a producer-consumer solution there? This is great! I need some time to process this! TYSM! Ref: https://www.powershellgallery.com/packages/Invoke-ProducerConsumer/1.0/Content/Invoke-ProducerConsumer.ps1 – Suraj Jun 24 '22 at 03:52
  • 1
    @Suraj didn't know about that thank you! looks pretty similar. Also the example from .NET Docs are great :) – Santiago Squarzon Jun 24 '22 at 03:56
  • @Suraj I have updated the example with something that should be more similar to what you're looking for – Santiago Squarzon Jun 24 '22 at 14:40
  • If the starting count of the queue (say, 2) is less than the max number of threads (say 5), then only that many (2, in this case) threads remain active no matter how many ever items are added to the queue later. Only the starting number of threads process the rest of the items in the queue. In my case, the starting count is usually one. Then I make a `irm` request, and add some 10~20 items. These are processed by only the first thread. The other threads go to Completed state right at the start. Is there a solution to this? – Suraj Jun 24 '22 at 14:40
  • @Suraj you're right, my answer is incomplete. I'll update it later. – Santiago Squarzon Jun 24 '22 at 15:55
  • 1
    `BlockingCollection` seems to to the solution for this. https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.blockingcollection-1?view=net-6.0 – Suraj Jun 25 '22 at 03:36