Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#
Tip/Trick

An AdjustableSemaphore for .NET

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
16 Jun 2020CPOL2 min read 6.5K   58   5   2
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)


Written By
Software Developer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionSealed? Disposable? Pin
Paulo Zemek16-Jun-20 20:41
mvaPaulo Zemek16-Jun-20 20:41 
AnswerRe: Sealed? Disposable? Pin
Thomas Roll16-Jun-20 22:29
Thomas Roll16-Jun-20 22:29 
- You can certainly mark it as sealed - I have not bothered in the past. I have used inheritance to implement custom functionality specific to solve challenges such as controlling rolling updates with a limited accepted failure rate etc

- In the sample app output you can see the available semaphore count go negative (see bold entries) - that is accepted by design. The requirement was to throttle the input stream (tasks being started up), not manipulate actively running tasks. An analogy would be adjusting the tap, limiting the amount of water being added to the sink - what water is in the sink and making its way through the plug hole is left as is.

- The class does not hold unmanaged resources, neither directly nor indirectly. So - in the form presented I do not see a need for IDisposable.
propodean - a podean who lost his amateur status

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.