Understanding Semaphore in .NET Core


Semaphore class in System.Threading is a thin wrapper around the Win32 Semaphore object. This is used to controls access to a resource or pool of resources concurrently.
SemaphoreSlim class is lightweight and faster than Semaphore, as it limited to a single process. Semaphore relies on synchronization primitives of CLR (Common Language Runtime).
SemaphoreSlim object is used to control the access to a resource like calling other API or limiting the I/O operations concurrently to avoid unnecessary network/hardware issues.
To understand more, will implement some real-time example where we need to call other 3rd party API for every request to our API. But to make it simple and to simulate the number of requests, we will create a loop here:
  1. HttpClient _httpClient = new HttpClient();  
  3. Public void Main(string[] args)  
  4. {  
  5.     Task.WaitAll(CallOtherAPI().ToArray());  
  6. }  
  8. public IEnumerable<Task> CallOtherAPI()  
  9. {  
  10.     for (int i = 0; i < 100; i++)  
  11.     {  
  12.         yield return CallAPI();  
  13.     }  
  14. }  
  16. public async Task CallAPI()  
  17. {  
  18.     try  
  19.     {  
  20.         var response = await _client.GetAsync("https://someapiurl.com");  
  21.         Console.WriteLine(response.StatusCode);  
  22.     }  
  23.     catch (Exception e)  
  24.     {  
  25.         Console.WriteLine(e.Message);  
  26.     }  
  27. }  
The operation was canceled.
The operation was canceled.
The operation was canceled.
In the above code, we are trying to call the same API in a loop of 100 to simulate 100 concurrent requests. In general, the network system is able to call any external services at a max of 20 to 30 requests at a given time. Any remaining requests will be blocked and will cancel. Due to this reason, when the output is observed, you might see that some of the requests were canceled. These requests are not canceled by 3rd party API, but our network system itself, as it was overloaded. To overcome such issues, we need to make sure to limit the request at any given time, even though we have a bunch of requests waiting.
This limitation of accessing the resources can be handled by a Semaphore object. Let modify the above example to limit the requests using a Semaphoreslim object.
  1. HttpClient _httpClient = new HttpClient();  
  2. SemaphoreSlim _semaphoregate = new SemaphoreSlim(1);  
  4. Public void Main(string[] args)  
  5. {  
  6.   Task.WaitAll(CallOtherAPI().ToArray());  
  7. }  
  9. public IEnumerable<Task> CallOtherAPI()  
  10. {  
  11.   for (int i = 0; i < 100; i++)  
  12.   {  
  13.     yield return CallAPI();  
  14.   }  
  15. }  
  17. public async Task CallAPI()  
  18. {  
  19.   try  
  20.   {  
  21.     await _semaphoregate.WaitAsync();  
  22.     var response = await _client.GetAsync("https://someapiurl.com");  
  23.     _semaphoregate.Release();  
  25.     Console.WriteLine(response.StatusCode);  
  26.   }  
  27.   catch (Exception e)  
  28.   {  
  29.     Console.WriteLine(e.Message);  
  30.   }  
  31. }  
Here, we introduced 3 more statements, let's understand each. Let's assume we are distributing an entry pass to a theatre. The rule is, at a given time only 50 people are allowed to be in a theatre. When people leave the theatre, they handover the pass in the counter so that it will be given to other people next. Similarly, SemaphoreSlim will hold the gate pass and keep distributing as it gets the pass to the requests. It might be one or many passes at a given time which can be configured via. Constructor while creating the SemaphoreSlim object. If we mention as 1, it will allow only one pass at a given time as below.
SemaphoreSlim _semaphoregate = new SemaphoreSlim(1);
When a person receives the pass, we need to decrease the count and it can be achieved by calling the WaitAsync() method which asynchronously blocks one pass and decreases the pool size of the SemaphoreSlim.
await _semaphoregate.WaitAsync();
Once the operation completes, or in our example terminology after seeing the movie, the person will handover the pass. In this case, we need to increase the count of a pool and it can be done using the Release() method.
This method also accepts one integer parameter to release more than one pass if needed. Now, the system can again allow another request to perform its operation as it releases. This way we can control the access to a resource and minimize the network cancellation issues.
Happy Coding :)