Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

An AdjustableSemaphore for .NET

5.00/5 (2 votes)
16 Jun 2020CPOL2 min read 7.4K   61  
A .NET equivalent of Java's AdjustableSemaphore
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:

C#
using System;
using System.Threading;

namespace CPSemaphore
{
    /// <summary>
    /// Implementation of an AdjustableSemaphore - similar to that provided 
    /// in the Java namesake, allowing for the Semaphore to be resizeddynamically 
    /// https://github.com/addthis/basis/blob/master/basis-core/src/main/java/com/
    /// addthis/basis/util/AdjustableSemaphore.java 
    /// .NET version originally published at the following URL 
    /// https://social.msdn.microsoft.com/Forums/vstudio/en-US/
    /// 5b648588-298b-452e-bc9a-1df0258242fe/
    /// how-to-implement-a-dynamic-semaphore?forum=netfxbcl 
    /// Minor alterations made, allowing for initialization using 0 as Semaphore size, 
    /// syntax and naming convention changes 
    /// </summary>

    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_AvailableCount 
                                        // can be < 0, resize will not affect active semaphores
                    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.

C#
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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)