The .NET Semaphore and SemaphoreSlim cannot be resized after initialisation - in Java, the AdjustableSemaphore has been available for some time which allows for the resizing of the Semaphore. In this tip, a .NET version of the AdjustableSemaphore is being presented.
Introduction
The Semaphore and SemaphoreSlim are useful and valuable components when it comes to controlling the number of active tasks/threads. However, the existing Semaphore and SemaphoreSlim implementations do not allow for resizing of a Semaphore once instantiated - so throttling of tasks/threads through the resizing of an instantiated Semaphore is not possible.
Background
The use of Semaphores as a method of limiting or throttling applications is a well known approach, however - in the .NET world, there is no way in which an instantiated Semaphore or SemaphoreSlim can be resized and thereby allowing for adjustments to the throttling to be made.
In Java, the AdjustableSemaphore
has been around for some time - and while looking at implementing this is .NET, I came across a series of posts discussing this exact requirement:
Adjusting for some minor functional changes, allowing for initialization of the AdjustableSemaphore
with a size of 0 (zero), a few naming and declaration updates and a fully functional AdjustableSemaphore
that fitted my needs came to light.
Using the Code
The AdjustableSemaphore
itself is a simple enough class:
using System;
using System.Threading;
namespace CPSemaphore
{
public class AdjustableSemaphore
{
private static readonly object m_LockObject = new object();
private int m_AvailableCount = 0;
private int m_MaximumCount = 0;
public AdjustableSemaphore(int maximumCount)
{
MaximumCount = maximumCount;
}
public int MaximumCount
{
get
{
lock (m_LockObject)
{
return m_MaximumCount;
}
}
set
{
lock (m_LockObject)
{
if (value < 0)
{
throw new ArgumentException("Must be greater than or equal to 0.",
"MaximumCount");
}
m_AvailableCount += value - m_MaximumCount;
m_MaximumCount = value;
Monitor.PulseAll(m_LockObject);
}
}
}
public void WaitOne()
{
lock (m_LockObject)
{
while (m_AvailableCount <= 0)
{
Monitor.Wait(m_LockObject);
}
m_AvailableCount--;
}
}
public void Release()
{
lock (m_LockObject)
{
if (m_AvailableCount < m_MaximumCount)
{
m_AvailableCount++;
Monitor.Pulse(m_LockObject);
}
else
{
throw new SemaphoreFullException("Adding the given count
to the semaphore would cause it to exceed its maximum count.");
}
}
}
public void GetSemaphoreInfo
(out int maxCount, out int usedCount, out int availableCount)
{
lock (m_LockObject)
{
maxCount = m_MaximumCount;
usedCount = m_MaximumCount - m_AvailableCount;
availableCount = m_AvailableCount;
}
}
}
}
In the sample code for this article, I have put together a little sample console application that shows how the dynamic change to the adjutableSemaphore
size works.
In this sample, an AdjustableSemaphore
is initialized with a size of 0 (zero) (AdjustableSemaphore semaphoreObject = new AdjustableSemaphore(0);
).
A total of ten tasks are started up, waiting for a Semaphore
.
The AdjustableSemaphore
is resized to 6
, (semaphoreObject.MaximumCount = 6;
), at which point the first 6 tasks will kick in, a processing will begin.
After a one second pause - the AdjustableSemaphore
is resized to 3
. All tasks running will carry on unaffected, but no new tasks will kick in until a free semaphore is available, which will not happen until the fourth task of the original 6 completes and releases.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CPSemaphore
{
class Program
{
static void Main(string[] args)
{
AdjustableSemaphore semaphoreObject = new AdjustableSemaphore(0);
List<task> tasks = new List<task>();
for (int i = 0; i< 10; ++i)
{
int j = i;
tasks.Add(Task.Factory.StartNew(() =>
{
Random rnd = new Random();
semaphoreObject.WaitOne();
semaphoreObject.GetSemaphoreInfo(out int maxCount,
out int usedCount, out int availableCount);
Console.WriteLine("{0} - Acquired Semaphore - Semaphore Max: {1},
Used: {2}, Available: {3}", j, maxCount, usedCount, availableCount);
Thread.Sleep(rnd.Next(1000, 5000));
semaphoreObject.Release();
semaphoreObject.GetSemaphoreInfo(out maxCount, out usedCount,
out availableCount);
Console.WriteLine("{0} - Released Semaphore - Semaphore Max: {1},
Used: {2}, Available: {3}", j, maxCount, usedCount, availableCount);
}));
}
semaphoreObject.MaximumCount = 6;
Console.WriteLine("Awaiting All Tasks");
Thread.Sleep(1000);
semaphoreObject.MaximumCount = 3;
Task.WaitAll(tasks.ToArray());
Console.WriteLine("All Tasks Complete");
Console.ReadLine();
}
}
}
The output of a run will look something like this:
Awaiting All Tasks
6 - Aquired Semaphore - Semaphore Max: 6, Used: 6, Available: 0
5 - Aquired Semaphore - Semaphore Max: 6, Used: 6, Available: 0
1 - Aquired Semaphore - Semaphore Max: 6, Used: 6, Available: 0
4 - Aquired Semaphore - Semaphore Max: 6, Used: 6, Available: 0
3 - Aquired Semaphore - Semaphore Max: 6, Used: 6, Available: 0
7 - Aquired Semaphore - Semaphore Max: 6, Used: 6, Available: 0
3 - Released Semaphore - Semaphore Max: 3, Used: 5, Available: -2
1 - Released Semaphore - Semaphore Max: 3, Used: 4, Available: -1
7 - Released Semaphore - Semaphore Max: 3, Used: 3, Available: 0
4 - Released Semaphore - Semaphore Max: 3, Used: 2, Available: 1
9 - Aquired Semaphore - Semaphore Max: 3, Used: 3, Available: 0
5 - Released Semaphore - Semaphore Max: 3, Used: 1, Available: 2
6 - Released Semaphore - Semaphore Max: 3, Used: 2, Available: 1
8 - Aquired Semaphore - Semaphore Max: 3, Used: 2, Available: 1
2 - Aquired Semaphore - Semaphore Max: 3, Used: 3, Available: 0
9 - Released Semaphore - Semaphore Max: 3, Used: 2, Available: 1
0 - Aquired Semaphore - Semaphore Max: 3, Used: 3, Available: 0
2 - Released Semaphore - Semaphore Max: 3, Used: 2, Available: 1
8 - Released Semaphore - Semaphore Max: 3, Used: 1, Available: 2
0 - Released Semaphore - Semaphore Max: 3, Used: 0, Available: 3
All Tasks Complete
As can be seen in the sample output, the resizing of the AdjustableSemaphore
from 6 to 3 result in a negative number of semaphores being available until the third task (number 7) has completed and released, at which point the available count is 0.
After task four (number 4) has completed, the available count is 1 and a new task (number 9) is run.
Points of Interest
The AdjustableSemaphore
will allow you to throttle processing by resizing the AdjustableSemaphore
dynamically, and it has come in handy for when I have had to implement dynamic scaling of processes in the past.
History
- 16th June, 2020: Initial version