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

Non Kernel Semaphore

Rate me:
Please Sign up or sign in to vote.
4.74/5 (9 votes)
11 Jun 2012CPOL3 min read 58.6K   12   53
A Semaphore that does not use the kernel by default

Introduction

I've been using threads a long time in .NET and honestly the topic interests me very much. When I began to get deeper into multithreading I came to use other threading constructs rather than just the Monitor (lock) and Interlocked classes. Interlocked and volatile mechanisms are user mode constructs meaning that they do not transition into the kernel. Kernel level objects like Mutex and Semaphores are created and managed by the operating system and are system wide. If we use a semaphore in our application we must leave user mode and transition into kernel mode just to create it. There is a slight performance hit that we incur since the kernel does not trust us. While this hit is negligible it can begin to cost a lot when used in different types of scenarios.

Background

I like the functionality that the semaphore provides however in some cases I would like to use it in just one process and not transition into kernel mode and take a hit. I've decided to write a very simple non-kernel semaphone.

What it has:

  1. Fast creation time since it just a regular .Net non-kernel object
  2. Only transitions to kernal mode when the maximum capacity has been met and more threads want to use it.

What it lacks:

  1. It's not system wide which makes sense because it 'mostly' stays in user mode.
  2. It does use Monitor which can transition into kernel mode, however this only occurs when all the slots have been filled.

The code

C++
public class NonKernalSemaphore
   {
       private readonly object padlock = new object();
       private readonly int maxSlots;
       private int usedSlots;


       public NonKernalSemaphore(int maxSlots)
       {
           this.maxSlots = maxSlots;
       }

       public void Enter()
       {
           lock (padlock)
           {
               while (usedSlots == maxSlots)
               {
                   Monitor.Wait(padlock);
               }

               usedSlots++;
           }
       }

       public void Release()
       {
           lock (padlock)
           {
               if (usedSlots > 0)
               {
                   usedSlots--;
                   Monitor.Pulse(padlock);
               }
           }
       }
   }

I wanted to keep this class as simple as possible without adding fluff so that I could cater for the basic usage scenario.

NonKernelSemaphore takes the maximum number of threads allowed at one time as constructor parameter maxSlots. It also keeps a count of how many threads are currently using the semaphore using the usedSlots field.

Enter Method Explanation

When a consumer calls the Enter() method a lock, padlock, is first accquired. We now check if there are any slots remaining in the semaphore. Obviously if usedSlots is equal to the maxSlots then all the slots are occupied. In this case we wait until another thread releases the slot by calling the Release() method. By calling Monitor.Wait() we transition into kernel mode and let the thread scheduler take care of things. Our scope at this point is still only process wide.

Release Method Explanation

Again in this method we must first call lock on the shared padlock object. We now decrease the usedSlots count which releases a slot in the semaphore. Then we awaken all the threads who are waiting on a slot by calling Monitor.PulseAll(). When the threads wake up they must first check if all the slots are used again hence the while condition (usedSlots == maxSlots).

Optimization

One optimization I was considering is only calling Monitor.Pulse() only if there are threads waiting for an available slot. This is definitely better than blindly calling the method. In this case we just need to keep track of how much many threads are waiting by introducing a new variable, waitingCount of type int incrementing it whenever a thread is waiting. The Release() method will then check if there are threads waiting. If there is then call Monitor.Pulse() otherwise just decrease usedSlots as usual. Here is the code:

C++
public class NonKernalSemaphore
   {
       private readonly object padlock = new object();
       private readonly int maxSlots;
       private int usedSlots;
       private int waitingCount;

       public NonKernalSemaphore(int maxSlots)
       {
           this.maxSlots = maxSlots;
       }

       public void Enter()
       {
           lock (padlock)
           {
               if (usedSlots == maxSlots)
               {
                   waitingCount++;

                   do
                   {
                       Monitor.Wait(padlock);

                   } while (usedSlots == maxSlots);

                   waitingCount--;
               }

               usedSlots++;
           }
       }

       public void Release()
       {
           lock (padlock)
           {
               if (usedSlots > 0)
               {
                   usedSlots--;

                   if (waitingCount > 0)
                   {
                       Monitor.Pulse(padlock);
                   }
               }
           }
       }
   }

Updates

Originally I used Monitor.PulseAll() however a Code Project reader ran some tests found that Pulse() was a bit faster.

Tests

Machine: Windows 7 Professional (64), Intel Xeon(R) 2.4 Ghz, 12 GB RAM, .Net 4.0, Release Mode

Test 1

Creation Time (Milliseconds) after 1 million iterations:

NonKernelSemaphore: 76

Semaphore: 2263

SemaphoreSlim: 623

Test 2

Enter/Wait/Release Time

Max concurrent slots: 8

Number of threads: 64

My test procedure included something like:

C++
public static void DoNonKernel()
 {
     nks.Enter();

     nksc++;
     nks.Release();
 }

 public static void DoSemaphore()
 {
     s.WaitOne();

     sc++;
     s.Release(1);
 }

 public static void DoSemaphoreSlim()
 {
     sl.Wait();

     slc++;
     sl.Release();
 }

I ran this a few times. Here are the results in milliseconds:

SemaphoreSlim:453.0453
Semaphore:241.0241
NonKernelSemaphore:249.0249

SemaphoreSlim:288.0288
Semaphore:249.0249
NonKernelSemaphore:249.0249

SemaphoreSlim:278.0278
Semaphore:264.0264
NonKernelSemaphore:285.0285

SemaphoreSlim:235.0235
Semaphore:273.0273
NonKernelSemaphore:246.0246

SemaphoreSlim:226.0226
Semaphore:237.0237
NonKernelSemaphore:217.0217

SemaphoreSlim:228.0228
Semaphore:224.0224
NonKernelSemaphore:381.0381

Points Of Interest

Feedbacks welcomed!

License

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


Written By
Software Developer (Senior) Finance Industry
United States United States
Currently pursuing 'Programming Nirvana' (The ineffable ultimate in which one has attained disinterested wisdom and compassion as it relates to programming)

Respected Technologies
1. Confusor (https://confuser.codeplex.com/)
2. Power Threading (http://www.wintellect.com/Resources/visit-the-power-threading-library)
3. EDI Parsers (http://www.rdpcrystal.com)


Acknowledgements:

Microsoft Certified Technologist for WPF and .Net 3.5 (MCTS)
Microsoft Certified Technologist for WCF and .Net 3.5 (MCTS)
Microsoft Certified Application Developer for .Net (MCAD)
Microsoft Certified Systems Engineer (MCSE)
Microsoft Certified Professional (MCP)

Sun Certified Developer for Java 2 Platform (SCD)
Sun Certified Programmer for Java 2 Platform (SCP)
Sun Certified Web Component Developer (SCWCD)

CompTIA A+ Certified Professional

Registered Business School Teacher for Computer Programming and Computer Applications (2004)
(University of the State of New York Education Department)

Graduated from University At Stony Brook

Comments and Discussions

 
SuggestionIssue Pin
Maxim Kartavenkov8-Oct-12 6:17
Maxim Kartavenkov8-Oct-12 6:17 
You have an issue with the locking. See: your both two methods performs locking on the object - both uses lock(padlock) statement, but in one method you are performing waiting operation inside lock, which will cause deadlocking in case if one method will be waiting inside Enter method in one thread and in other thread you will call Release. This is a major issue in multithreading applications - waiting for a long period inside critical sections should be avoided, but your code can wait infinite.

Maxim.
GeneralRe: Issue Pin
FatCatProgrammer8-Oct-12 13:25
FatCatProgrammer8-Oct-12 13:25 
GeneralRe: Issue Pin
Maxim Kartavenkov8-Oct-12 21:12
Maxim Kartavenkov8-Oct-12 21:12 
GeneralRe: Issue Pin
FatCatProgrammer9-Oct-12 13:52
FatCatProgrammer9-Oct-12 13:52 
GeneralMy vote of 3 Pin
ozbear18-Jun-12 13:48
ozbear18-Jun-12 13:48 
GeneralRe: My vote of 3 Pin
FatCatProgrammer18-Jun-12 16:26
FatCatProgrammer18-Jun-12 16:26 
Questionlock statement is just a disguised Monitor.Enter Pin
ozbear18-Jun-12 13:46
ozbear18-Jun-12 13:46 
AnswerRe: lock statement is just a disguised Monitor.Enter Pin
FatCatProgrammer18-Jun-12 16:25
FatCatProgrammer18-Jun-12 16:25 
GeneralRe: lock statement is just a disguised Monitor.Enter Pin
ozbear19-Jun-12 15:23
ozbear19-Jun-12 15:23 
GeneralRe: lock statement is just a disguised Monitor.Enter Pin
FatCatProgrammer20-Jun-12 1:07
FatCatProgrammer20-Jun-12 1:07 
GeneralMy vote of 4 Pin
oggenok6411-Jun-12 12:45
oggenok6411-Jun-12 12:45 
GeneralMy vote of 4 Pin
Vitaly Tomilov10-Jun-12 7:50
Vitaly Tomilov10-Jun-12 7:50 
GeneralRe: My vote of 4 Pin
FatCatProgrammer11-Jun-12 4:24
FatCatProgrammer11-Jun-12 4:24 
GeneralMy vote of 4 Pin
Paulo Zemek6-Jun-12 10:59
mvaPaulo Zemek6-Jun-12 10:59 
GeneralRe: My vote of 4 Pin
FatCatProgrammer6-Jun-12 13:05
FatCatProgrammer6-Jun-12 13:05 
QuestionGood, but... Pin
Paulo Zemek6-Jun-12 6:40
mvaPaulo Zemek6-Jun-12 6:40 
AnswerRe: Good, but... Pin
FatCatProgrammer6-Jun-12 7:41
FatCatProgrammer6-Jun-12 7:41 
GeneralRe: Good, but... Pin
Paulo Zemek6-Jun-12 7:50
mvaPaulo Zemek6-Jun-12 7:50 
GeneralRe: Good, but... Pin
Paulo Zemek6-Jun-12 8:47
mvaPaulo Zemek6-Jun-12 8:47 
GeneralRe: Good, but... Pin
FatCatProgrammer6-Jun-12 9:15
FatCatProgrammer6-Jun-12 9:15 
GeneralRe: Good, but... Pin
Paulo Zemek6-Jun-12 10:54
mvaPaulo Zemek6-Jun-12 10:54 
SuggestionGood article, but expecting more Pin
Sridhar Patnayak6-Jun-12 1:16
professionalSridhar Patnayak6-Jun-12 1:16 
GeneralRe: Good article, but expecting more Pin
FatCatProgrammer6-Jun-12 3:28
FatCatProgrammer6-Jun-12 3:28 
GeneralRe: Good article, but expecting more Pin
FatCatProgrammer6-Jun-12 6:28
FatCatProgrammer6-Jun-12 6:28 
GeneralMy vote of 1 Pin
Yaroslav Tatarenko4-Jun-12 20:34
Yaroslav Tatarenko4-Jun-12 20:34 

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.