Exploring SemaphoreSlim Configurations with C# Examples

Concurrency control is a critical part of building responsive and scalable applications. In .NET, SemaphoreSlim is a lightweight synchronization primitive that helps manage access to a limited resource pool. But how do different constructor parameters affect its behavior?

In this post, we’ll explore four common SemaphoreSlim configurations using a visual diagram and a hands-on C# code example to demonstrate how they behave under load.

What Is SemaphoreSlim?

SemaphoreSlim is used to limit the number of threads that can access a resource concurrently. It takes two parameters:

SemaphoreSlim(int initialCount, int maxCount)
  • initialCount: How many threads can enter immediately.
  • maxCount: The maximum number of threads that can be allowed concurrently.

Visualizing SemaphoreSlim Configurations

Here’s a visual representation of how different configurations behave:

  • Blue bars: Initial available slots (how many threads can enter immediately)
  • Gray bars: Remaining capacity (how many more threads can be released into the semaphore)

Code Example

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        var semaphores = new[]
        {
            new SemaphoreSlim(1),            
            new SemaphoreSlim(1, 10), 
            new SemaphoreSlim(10, 10),
            new SemaphoreSlim(10, 20) 
        };

        for (int i = 0; i < semaphores.Length; i++)
        {
            Console.WriteLine($"\nTesting Semaphore {i + 1}");
            var semaphore = semaphores[i];

            var tasks = new Task[15];
            for (int j = 0; j < tasks.Length; j++)
            {
                int taskNum = j;
                tasks[j] = Task.Run(async () =>
                {
                    if (await semaphore.WaitAsync(TimeSpan.FromMilliseconds(500)))
                    {
                        Console.WriteLine($"Task {taskNum} entered Semaphore {i + 1}");
                        await Task.Delay(200); // Simulate work
                        semaphore.Release();
                    }
                    else
                    {
                        Console.WriteLine($"Task {taskNum} timed out on Semaphore {i + 1}");
                    }
                });
            }

            await Task.WhenAll(tasks);
        }
    }
}

Expected Output

1. SemaphoreSlim(1)

  • Only 1 task enters at a time.
  • The rest wait or timeout.
  • Output: ~1 task enters every 200ms; others may timeout depending on timing.

2. SemaphoreSlim(1, 10)

  • Starts with 1 slot.
  • As tasks release, others can enter.
  • Up to 10 can be released before hitting maxCount.

3. SemaphoreSlim(10, 10)

  • 10 tasks enter immediately.
  • Remaining 5 wait or timeout.

4. SemaphoreSlim(10, 20)

  • Same as above, but allows up to 20 concurrent entries if released enough.

SemaphoreSlim is a lightweight synchronization primitive in .NET designed to control access to a limited resource within a single process. It’s optimized for performance and supports asynchronous operations, making it ideal for modern applications that rely on async and await. Unlike traditional locks, SemaphoreSlim allows you to specify both an initial count and a maximum count, enabling fine-grained control over concurrency. However, when you need to coordinate access across multiple processes, SemaphoreSlim falls short—this is where the classic Semaphore comes in.

SemaphoreSlim vs Semaphore: Key Differences

Feature SemaphoreSlim Semaphore
Purpose Lightweight semaphore for managing concurrent access within a single process. Kernel-based semaphore for inter-process synchronization.
Implementation Purely managed code (no OS kernel object). Wraps an OS kernel semaphore object.
Performance Faster and more efficient for in-process usage (less overhead). Slower due to kernel transitions and inter-process capabilities.
Inter-process support ❌ No (works only within the same process). ✅ Yes (can synchronize across processes).
Async support ✅ Yes (WaitAsync for async operations). ❌ No built-in async support.
Constructor parameters SemaphoreSlim(initialCount, maxCount) Semaphore(initialCount, maxCount, name)
Resource usage Lower memory footprint, no kernel handles. Higher resource usage (kernel handles).
Use case Ideal for limiting concurrency in async code or within a single app domain. Needed when multiple processes must coordinate access to shared resources.


Closing Summary:
Use SemaphoreSlim for in-process, async-friendly scenarios (e.g., throttling tasks in a web app). Use Semaphore when you need cross-process synchronization (e.g., multiple apps accessing the same file).

The Past, Present and Future of The Blog

The Past

I started this blog just over 10 years ago. The original purpose was twofold, firstly to have a digital version of all the programming notes collected over the previous 15 years while developing applications in classic ASP, VB6 and then later in VB/ASP.Net back when the .Net Framework was new. I had built a habit of keeping a note of any code I found useful. A lot of the notes were either printed copies of code I had written for an application or snippets of code I had written in an A5 ring bound pad sporting a blue denim cover. Even today I still have that same pad with the many folded A4 printed sheets stuffed in the front, although none of it is relevant to modern technologies or languages I currently use today I keep it for nostalgia as part of my career history. The second reason for starting the blog was to record and share programming notes for new technologies as and when I learn them going forward. I needed making notes to be easy to do, easy to access, easy to search and something that was minimal or even no cost. Although there are so, so many options that fit the criteria I was sold on choosing WordPress for two main reasons, it was free to get started and it was easy to use.

Right at the beginning I came up with the name DotNetable (as in .Net able) which made a lot of sense to me as I was a developer, primarily using and blogging about Microsoft coding and technologies. Originally the domain dotnetable[dot]com was purchased and the site run under that domain for a while. At some point the registration was not renewed by myself so it has acquired a new owner.

Everything started out well enough and met the intended purposes. World domination was never the focus so I have never actively promoted the blog myself. There have been a few occasions where I may have given a developer colleague the link to a post that helps them with something they are working on, but nothing more.

It has been a little over 5 years since the last post was published. Although it goes without saying (oh I can’t help myself) “A lot has happened in that time”. There has been, and continues to be, a number of advancements in many areas of Robotics and AI. Generative AI being the big talking point of the last couple of years. There have been numerous advancements by Microsoft in cloud computing with Azure and their development tools with the recent release of C# 12 and .Net 8.

The Present

Even though there have been no new posts in 5 years the site is still has an average of 40 views per day. I realise that is not a lot, but people are still visiting. A famous line from the 1989 film The Field of Dreams comes to mind “If you build it, he will come.” 

The site is already had some changes.  As the previously mentioned domain is no longer available new domain has been registered and linked to the site. The theme has been changed.

The Future. The road ahead?

The plan is simple, to be making notes like I was originally for my own reference and for the benefit of anyone else who happens to find themselves on the site (but mainly mine).  There are number of topics I intend to learn/practise including but not limited to,

  • General Machine Learning/ML.Net
  • AI
  • .Net 8
  • C#
  • Azure
  • Devops
  • Design Patterns
  • Software Principles e.g. SOLID
  • Useful nuget packages.
  • etc…..

Pretty much anything in any way related to software development for example

  • Python
  • Raspberry Pi/Arduino
  • SQL
  • Javascript
  • etc…..

So there we have it. I look forward to seeing you soon….

C# 6 Read-only auto properties

A while ago, when I was looking at read-only auto-properties I started comparing them to the previous method of making a property read-only by making it private. When it comes down to it there are a few differences how they can be initialised and read/written to. At the time I put together this example. Comments explain where and how each property can be accessed.

void Main()
{
	Person firstPerson=new Person(new DateTime(2001, 10, 16));
	
	// Unrestricted access to FirstName and LastName
	firstPerson.FirstName="John";
	firstPerson.LastName="Jones";
	
	// Cannot access Date Of Birth, other than via consructor or within class membes internally
	//firstPerson.DateOfBirth= new DateTime(1926, 03, 27);
	//firstPerson.Age = 34;
		
	// Accessing readonly property directly produces a compile time error
	firstPerson.ChangeDateOfBirth(new DateTime(1926, 03, 27));
	
	// Uncomment if runnning with LinqPad
	//Console.WriteLine(firstPerson);;
}

public class Person{

	// Unrestricted access
	public string FirstName{get; set;}	
	public string LastName{get; set;}	
	
	// Accessible only by Constructor
	public DateTime DateOfBirth {get;} // Date
	
	// Accessible by Contructors and internally to the class members
	public int Age {get; private set;}
	
	
	public Person(DateTime dob){
		DateOfBirth = dob;
		
		Age=-1;
		CalculateAge();
	}
			
	public void CalculateAge(){
		// Validat Date set before calculation
	
		// Implementation to calculate the age from the DateOfBirth
		DateTime today = DateTime.Today;
		Age = today.Year - DateOfBirth.Year;
	    if (today.Month < DateOfBirth.Month || (today.Month == DateOfBirth.Month && today.Day < DateOfBirth.Day))
		{
        	Age--;
		}
	}
	
	public void ChangeDateOfBirth(DateTime dateOfBirth){
		// Compile time error when trying to access readonly property
		//DateOfBirth = dateOfBirth;
	}
}

I have made this example available as a Gist here

Object Serialization and Deserialzation in c#

Previously I have written about how to serialize an object to XML. This post is an improved version of that post with better code and the addition of a deserialize example.
Required Namespace to be imported for serialization is System.Xml.Serialization.

This example is also available as a gist

public class Person
{
    public string Name {get; set;}
    public int Age {get;set;}
    public Address Address{get;set;}
}

public class Address
{
    public string Address1 {get;set;}
    public string Town {get;set;}
    public string PostCode{get;set;}
}

void Main()
{
    var p = new Person{
        Name = "John Jones",
        Age = 40,
        Address = new Address{
        Address1= "Daisy Meadow",
        Town="Chorville",
        PostCode = "CH1 1HC"
    }
};
	
XmlSerializer xmlSerializer = new XmlSerializer(p.GetType());
	
var xmlText = string.Empty;
using (TextWriter textWriter = new StringWriter()){
    xmlSerializer.Serialize(textWriter, p);
    xmlText = textWriter.ToString();
}

XmlReaderSettings settings = new XmlReaderSettings();
using(StringReader textReader = new StringReader(xmlText)) {
    using(XmlReader xmlReader = XmlReader.Create(textReader, settings)) {
         ((Person)xmlSerializer.Deserialize(xmlReader)).Dump();
        }
    }
}

Handling Complex Fake or Test Data When Unit Testing-Builder Pattern

The data supporting the unit tests you are creating may have classes that contain other classes as properties. Here we have a Person class that contains an Address class

public class Person{
	public string FirstName {get;set;} 
	public string LastName {get;set;}
	public Address Address{get;set;}
}

public class Address{
	public string Address1{get;set;}
	public string PostCode{get;set;}
	public string Country{get;set;}
}

There are a number of possible scenarios to be unit tested, i.e. If the address is missing or invalid or missing person details. For each unit test the test objects will need to be created which could easily result in duplicate or just tedious typing. For example objects similar to the one below may need to be created with slightly different data for each test.

var person = new Person{
	FirstName="Joe",
	LastName="Bloggs",
	Address=new Address{
		Address1="1 The Grove",
		PostCode="PembroVille",
		Country="United Kingdom"	
	}
};

A real life scenario could involve more complex classes than this example resulting in more tedious duplication. One useful trick is to create a builder class to easily build objects that cater for different test scenarios. In this builder class you can configure the data for each different positive and negative test with the advantages of configure once, reusable code.
Below is a PersonBuilder class which aids in building a Person object with different data relevant to the tests being performed.

public class PersonBuilder
{
    private Person _person = new Person();

    public Person Build(){
        // Set standard default values for properties here if needed
        return _person;
    }

    public PersonBuilder WithValidDetails()
    {
        _person.FirstName="Joe";
        _person.LastName="Bloggs";
        return this;
    }

    public PersonBuilder WithValidAddress()
    {
        _person.Address=new Address{
            Address1="1 The Grove",
            PostCode="PembroVille",
            Country="United Kingdom"
        };
        return this;
    }
	
    public PersonBuilder WithInValidAddress()
    {
        _person.Address=null;
        return this;
    }
}

I can build an empty Person

	var person = new PersonBuilder().WithValidDetails().Build();

A Person with valid details and an valid address

var person = new PersonBuilder().WithValidDetails().WithValidAddress().Build();

This is a somewhat simplified example which would work easily well with complex classes.