Need to understand the usage of SemaphoreSlim
Asked Answered
C

4

158

Here is the code I have but I don't understand what SemaphoreSlim is doing.

async Task WorkerMainAsync()
{
    SemaphoreSlim ss = new SemaphoreSlim(10);
    List<Task> trackedTasks = new List<Task>();
    while (DoMore())
    {
        await ss.WaitAsync();
        trackedTasks.Add(Task.Run(() =>
        {
            DoPollingThenWorkAsync();
            ss.Release();
        }));
    }
    await Task.WhenAll(trackedTasks);
}

void DoPollingThenWorkAsync()
{
    var msg = Poll();
    if (msg != null)
    {
        Thread.Sleep(2000); // process the long running CPU-bound job
    }
}

What do await ss.WaitAsync(); and ss.Release(); do?

I guess that if I run 50 threads at a time then write code like SemaphoreSlim ss = new SemaphoreSlim(10); then it will be forced to run 10 active thread at time.

When one of 10 threads completes then another thread will start. If I am not right then help me to understand with sample situation.

Why is await needed along with ss.WaitAsync();? What does ss.WaitAsync(); do?

Confine answered 18/11, 2013 at 20:2 Comment(2)
One thing to note is that you really should wrap that "DoPollingThenWorkAsync();" in a "try { DoPollingThenWorkAsync(); } finally { ss.Release(); }", otherwise exceptions will permanently starve that semaphore.Coumarin
I feel a bit strange that we acquire and release the semaphore outside/inside the task respectively. Will moving the "await ss.WaitAsync()" inside the task make any difference?Orcinol
S
104

i guess that if i run 50 thread at a time then code like SemaphoreSlim ss = new SemaphoreSlim(10); will force to run 10 active thread at time

That is correct; the use of the semaphore ensures that there won't be more than 10 workers doing this work at the same time.

Calling WaitAsync on the semaphore produces a task that will be completed when that thread has been given "access" to that token. await-ing that task lets the program continue execution when it is "allowed" to do so. Having an asynchronous version, rather than calling Wait, is important both to ensure that the method stays asynchronous, rather than being synchronous, as well as deals with the fact that an async method can be executing code across several threads, due to the callbacks, and so the natural thread affinity with semaphores can be a problem.

A side note: DoPollingThenWorkAsync shouldn't have the Async postfix because it's not actually asynchronous, it's synchronous. Just call it DoPollingThenWork. It will reduce confusion for the readers.

Socorrosocotra answered 18/11, 2013 at 20:6 Comment(26)
thanks but please tell me what happen when we specify no of thread to run say 10.when one of 10 thread finish then again that thread jump onto finish another jobs or goes back to pool? this is not very clear to....so please explain what happen behind the scene.Confine
@Confine What's not clear about it? The code waits until there are less than 10 tasks currently running; when there are, it adds another. When a task finishes it indicates that it has finished. That's it.Socorrosocotra
what is the advantage of specifying no of thread to run. if too many thread may hamper performance ? if yes then why hamper...if i run 50 threads instead of 10 thread then why performance will matter...can u please explain. thanksRestore
@Restore If you have too many concurrent threads then the threads spend more time context switching than they spend doing productive work. Throughput goes down as threads go up as you spend more and more time managing threads instead of doing work, at least, once your thread count goes much past the number of cores on the machine.Socorrosocotra
@Socorrosocotra That's part of the job of the task scheduler. Tasks != threads. The Thread.Sleep in the original code would devastate the task scheduler. If you're not async to the core, you're not async.Depose
@JosephLennox As the comment indicates, the Thread.Sleep is just a placeholder for actual CPU bound work.Socorrosocotra
Servy, so to clarify, if it was I/O bound work and awaiting properly can we leave this to the task scheduler like @JosephLennox says, i.e. not use SempaphoreSlimArcane
@Arcane Joseph wasn't saying that one shouldn't be using a semaphore, he was saying that the performance benefits of making the code asynchronous is defeated one is synchronously blocking on IO bound work. If one isn't doing that, and makes the whole thing asynchronous, there are potential scaling benefits. The ability to not consume a worker thread to wait in no way means you don't need the Semaphore.Socorrosocotra
I'm imagining the DoPollingThenWorkAsync was doing await Task.Delay(1000); instead (or more real world POSTing a set of data) and returning Task. Would you then worry about throttling in this way? Looks like you're saying yes?Arcane
If that was the work to be done then there'd be no reason to wrap it in a Task.Run call, so that could be removed (and the IO operation would be awaited instead) and that'd be all that would need to change. What the OP is doing is appropriate for the situation he described (having CPU bound work). If he didn't have CPU bound work, he'd no longer need to tie up a thread. The throttling has nothing to do with whether the work is CPU or IO bound; it'll stay the same either way, if that's the requirement.Socorrosocotra
Couldn't he have just as easily put his ss.Release() after his await Task.WhenAll?Lyophilic
@Lyophilic If they want the code to deadlock and never finish, they could do that.Socorrosocotra
@Socorrosocotra Haha. Yes, you're right. How silly of me. He probably doesn't want that.Lyophilic
@Socorrosocotra ~ If we change DoPollingThenWork() to Async and remove the Task.Run() call, as you suggest, where then should we call ss.Release()? Also, how then to do the Try/Finally?Silicone
@Socorrosocotra ~ OK, I think I got it. I put the Try/Finally and ss.Release() call in the body of the DoPollingThenWorkAsync() function. Agreed?Silicone
@Silicone I would consider it better to keep the actual business logic of whatever work is being done separated from the code managing scheduling the workers and deciding what can run, and when, so no, I wouldn't have the actual business logic be involved in actually interacting with the semaphore directly.Socorrosocotra
Fair 'nuff. Makes sense. Where, then, would you prefer to put the Try/Finally and release the semaphore? Perhaps create a function another level down for the actual business logic?Silicone
10 active threads <== this is misleading. When awaiting an asynchronous operation there is no "active thread" waiting for the result. "Active waiting" is a contradiction by itself anyway.Approach
" ... keep the actual business logic of whatever work is being done separated from the code managing scheduling ... " Since we're reduced to a one-line call when we convert it to Async, just about the only place to put the Try/Finally and Release() will be in a function downstream of that call. For purposes of separation of concerns, then, as you mention, the place for the worker code will be in yet another function downstream from that. I believe I'll take that approach the next time I run into this, unless you've a cautionary note against it that you'd like others to consider.Silicone
@Silicone "Perhaps create a function another level down" that would work.Socorrosocotra
@TheodorZoulias Any code waiting to get into the semaphore isn't counted as one of the "10 active threads". Those 10 active threads are the ones that have already gotten one of the semaphore tokens and are actually running business logic on a thread pool therad.Socorrosocotra
The typical use of SemaphoreSlim.WaitAsync is for throttling asynchronous I/O-bound operations, and these operations typically don't employ threads. In this scenario a hot task is not associated with a running or blocked thread. There is no need for a thread to be blocked while a remote server is cooking a response, or while the network driver is receiving the said response. You should read the famous There Is No Thread article by Stephen Cleary for details.Approach
@TheodorZoulias WaitAsync is not solely for synchronizing IO bound operations. It's for synchronizing anything for which you want to have the waiting code wait asynchronously, not synchronously. Again, the code that is waiting for a token from the semaphore is not blocking any threads. It's the 10 code paths that have been given a semaphore token that are doing work, and we know that the work is CPU bound thread performed by a worker task because that's what the requirements state needs to be synchronized.Socorrosocotra
As long as you talk about "code paths" it's OK. "Asynchronous workflows" is also a suitable term. But "active threads" is misleading. Someone could assume that some inactive threads became active after the awaiting. In the OP's example there are indeed CPU-bound jobs that will normally employ one thread each, but these jobs are not the continuations of SemaphoreSlim.WaitAsync. They are just scheduled by these continuations. The code path after await ss.WaitAsync() will probably employ a thread for just a couple of μsec before releasing it back to the thread pool.Approach
@TheodorZoulias Again, the only time I've talked about threads, from the start, has been with respect to the workers after they have been given a token from the semaphore. I have never once stated that threads are waiting to take a semaphore token, you've merely asserted, incorrectly, that I have.Socorrosocotra
My point is that the text is misleading. It can easily create incorrect assumptions to anyone who is in the process of learning about threads, tasks, async-await and all this stuff. IMHO it will be better if you replace the phrase "when that thread" with "when that worker".Approach
B
365

In the kindergarden around the corner they use a SemaphoreSlim to control how many kids can play in the PE room.

They painted on the floor, outside of the room, 5 pairs of footprints.

As the kids arrive, they leave their shoes on a free pair of footprints and enter the room.

Once they are done playing they come out, collect their shoes and "release" a slot for another kid.

If a kid arrives and there are no footprints left, they go play elsewhere or just stay around for a while and check every now and then (i.e., no FIFO priorities).

When a teacher is around, she "releases" an extra row of 5 footprints on the other side of the corridor such that 5 more kids can play in the room at the same time.

It also has the same "pitfalls" of SemaphoreSlim...

If a kid finishes playing and leaves the room without collecting the shoes (does not trigger the "release") then the slot remains blocked, even though there is theoretically an empty slot. The kid usually gets told off, though.

Sometimes one or two sneaky kid hide their shoes elsewhere and enter the room, even if all footprints are already taken (i.e., the SemaphoreSlim does not "really" control how many kids are in the room).

This does not usually end well, since the overcrowding of the room tends to end in kids crying and the teacher fully closing the room.

Balough answered 21/12, 2019 at 10:44 Comment(4)
These kinds of answers are my favorites.Neurovascular
love it! a teacher from godWattenberg
Please explain how is this possible with SemaphoreSlim? "the SemaphoreSlim does not "really" control how many kids are in the room"Lamont
If it's an unmanaged resource that you're locking, then SemaphoreSlim will not obtain an OS-level lock (hence the "slim"). It can only block the entrance it controls directly.Edita
S
104

i guess that if i run 50 thread at a time then code like SemaphoreSlim ss = new SemaphoreSlim(10); will force to run 10 active thread at time

That is correct; the use of the semaphore ensures that there won't be more than 10 workers doing this work at the same time.

Calling WaitAsync on the semaphore produces a task that will be completed when that thread has been given "access" to that token. await-ing that task lets the program continue execution when it is "allowed" to do so. Having an asynchronous version, rather than calling Wait, is important both to ensure that the method stays asynchronous, rather than being synchronous, as well as deals with the fact that an async method can be executing code across several threads, due to the callbacks, and so the natural thread affinity with semaphores can be a problem.

A side note: DoPollingThenWorkAsync shouldn't have the Async postfix because it's not actually asynchronous, it's synchronous. Just call it DoPollingThenWork. It will reduce confusion for the readers.

Socorrosocotra answered 18/11, 2013 at 20:6 Comment(26)
thanks but please tell me what happen when we specify no of thread to run say 10.when one of 10 thread finish then again that thread jump onto finish another jobs or goes back to pool? this is not very clear to....so please explain what happen behind the scene.Confine
@Confine What's not clear about it? The code waits until there are less than 10 tasks currently running; when there are, it adds another. When a task finishes it indicates that it has finished. That's it.Socorrosocotra
what is the advantage of specifying no of thread to run. if too many thread may hamper performance ? if yes then why hamper...if i run 50 threads instead of 10 thread then why performance will matter...can u please explain. thanksRestore
@Restore If you have too many concurrent threads then the threads spend more time context switching than they spend doing productive work. Throughput goes down as threads go up as you spend more and more time managing threads instead of doing work, at least, once your thread count goes much past the number of cores on the machine.Socorrosocotra
@Socorrosocotra That's part of the job of the task scheduler. Tasks != threads. The Thread.Sleep in the original code would devastate the task scheduler. If you're not async to the core, you're not async.Depose
@JosephLennox As the comment indicates, the Thread.Sleep is just a placeholder for actual CPU bound work.Socorrosocotra
Servy, so to clarify, if it was I/O bound work and awaiting properly can we leave this to the task scheduler like @JosephLennox says, i.e. not use SempaphoreSlimArcane
@Arcane Joseph wasn't saying that one shouldn't be using a semaphore, he was saying that the performance benefits of making the code asynchronous is defeated one is synchronously blocking on IO bound work. If one isn't doing that, and makes the whole thing asynchronous, there are potential scaling benefits. The ability to not consume a worker thread to wait in no way means you don't need the Semaphore.Socorrosocotra
I'm imagining the DoPollingThenWorkAsync was doing await Task.Delay(1000); instead (or more real world POSTing a set of data) and returning Task. Would you then worry about throttling in this way? Looks like you're saying yes?Arcane
If that was the work to be done then there'd be no reason to wrap it in a Task.Run call, so that could be removed (and the IO operation would be awaited instead) and that'd be all that would need to change. What the OP is doing is appropriate for the situation he described (having CPU bound work). If he didn't have CPU bound work, he'd no longer need to tie up a thread. The throttling has nothing to do with whether the work is CPU or IO bound; it'll stay the same either way, if that's the requirement.Socorrosocotra
Couldn't he have just as easily put his ss.Release() after his await Task.WhenAll?Lyophilic
@Lyophilic If they want the code to deadlock and never finish, they could do that.Socorrosocotra
@Socorrosocotra Haha. Yes, you're right. How silly of me. He probably doesn't want that.Lyophilic
@Socorrosocotra ~ If we change DoPollingThenWork() to Async and remove the Task.Run() call, as you suggest, where then should we call ss.Release()? Also, how then to do the Try/Finally?Silicone
@Socorrosocotra ~ OK, I think I got it. I put the Try/Finally and ss.Release() call in the body of the DoPollingThenWorkAsync() function. Agreed?Silicone
@Silicone I would consider it better to keep the actual business logic of whatever work is being done separated from the code managing scheduling the workers and deciding what can run, and when, so no, I wouldn't have the actual business logic be involved in actually interacting with the semaphore directly.Socorrosocotra
Fair 'nuff. Makes sense. Where, then, would you prefer to put the Try/Finally and release the semaphore? Perhaps create a function another level down for the actual business logic?Silicone
10 active threads <== this is misleading. When awaiting an asynchronous operation there is no "active thread" waiting for the result. "Active waiting" is a contradiction by itself anyway.Approach
" ... keep the actual business logic of whatever work is being done separated from the code managing scheduling ... " Since we're reduced to a one-line call when we convert it to Async, just about the only place to put the Try/Finally and Release() will be in a function downstream of that call. For purposes of separation of concerns, then, as you mention, the place for the worker code will be in yet another function downstream from that. I believe I'll take that approach the next time I run into this, unless you've a cautionary note against it that you'd like others to consider.Silicone
@Silicone "Perhaps create a function another level down" that would work.Socorrosocotra
@TheodorZoulias Any code waiting to get into the semaphore isn't counted as one of the "10 active threads". Those 10 active threads are the ones that have already gotten one of the semaphore tokens and are actually running business logic on a thread pool therad.Socorrosocotra
The typical use of SemaphoreSlim.WaitAsync is for throttling asynchronous I/O-bound operations, and these operations typically don't employ threads. In this scenario a hot task is not associated with a running or blocked thread. There is no need for a thread to be blocked while a remote server is cooking a response, or while the network driver is receiving the said response. You should read the famous There Is No Thread article by Stephen Cleary for details.Approach
@TheodorZoulias WaitAsync is not solely for synchronizing IO bound operations. It's for synchronizing anything for which you want to have the waiting code wait asynchronously, not synchronously. Again, the code that is waiting for a token from the semaphore is not blocking any threads. It's the 10 code paths that have been given a semaphore token that are doing work, and we know that the work is CPU bound thread performed by a worker task because that's what the requirements state needs to be synchronized.Socorrosocotra
As long as you talk about "code paths" it's OK. "Asynchronous workflows" is also a suitable term. But "active threads" is misleading. Someone could assume that some inactive threads became active after the awaiting. In the OP's example there are indeed CPU-bound jobs that will normally employ one thread each, but these jobs are not the continuations of SemaphoreSlim.WaitAsync. They are just scheduled by these continuations. The code path after await ss.WaitAsync() will probably employ a thread for just a couple of μsec before releasing it back to the thread pool.Approach
@TheodorZoulias Again, the only time I've talked about threads, from the start, has been with respect to the workers after they have been given a token from the semaphore. I have never once stated that threads are waiting to take a semaphore token, you've merely asserted, incorrectly, that I have.Socorrosocotra
My point is that the text is misleading. It can easily create incorrect assumptions to anyone who is in the process of learning about threads, tasks, async-await and all this stuff. IMHO it will be better if you replace the phrase "when that thread" with "when that worker".Approach
U
11

Although I accept this question really relates to a countdown lock scenario, I thought it worth sharing this link I discovered for those wishing to use a SemaphoreSlim as a simple asynchronous lock. It allows you to use the using statement which could make coding neater and safer.

http://www.tomdupont.net/2016/03/how-to-release-semaphore-with-using.html

I did swap _isDisposed=true and _semaphore.Release() around in its Dispose though in case it somehow got called multiple times.

Also it is important to note SemaphoreSlim is not a reentrant lock, meaning if the same thread calls WaitAsync multiple times the count the semaphore has is decremented every time. In short SemaphoreSlim is not Thread aware.

Regarding the questions code-quality it is better to put the Release within the finally of a try-finally to ensure it always gets released.

Unwish answered 19/5, 2018 at 9:18 Comment(1)
It's inadvisable to post link-only answers since links tend to die over time thus rendering the answer worthless. If you can, it's best to summarise the key points or key code block into your answer.Leesaleese
J
2

Important: Don't assume WaitAsync throws an exception if it times out

If the semaphore has available threads remaining (where CacheSemaphore.CurrentCount > 0) then await CacheSemaphore.WaitAsync(5000) will decrement that count and return immediately.

However, if CacheSemaphore.CurrentCount == 0 is already true, then await CacheSemaphore.WaitAsync(5000) will wait 5 seconds and that's it! Your code will continue anyway and I'm sure I'm not the only one who assumed it would throw an exception.

So what this means is if you have 100 tasks 'protected from running' with WaitAsync(5000) and a semaphore with a maximum size of 1 then without due care they'll all get to continue after 5 seconds. That probably isn't what you want.

What are you supposed to do?

If using WaitAsync with a timeout you need to check the value returned to see whether or not you actually acquired the lock.

if (await CacheSemaphore.WaitAsync(5000) == true)
{
  // ok to run the task
  // release lock
}
else
{
  // not ok to run the task
}

Obviously the == true isn't actually needed, but I like the explicitness.

What's an even worse mistake you can make?

It's good practice to Release() the semaphore inside a try/finally block. That ensures it is always released whether it fails or succeeds.

However if using a timeout, it's very important not to accidentally Release() a lock you never obtained:

Don't do this:

try {

    if (await CacheSemaphore.WaitAsync(5000) == true)
    {
      // ok to run the task
    }
    else
    {
      // not ok to run the task
    }
}
finally
{
    // `Release` just blindly increments the counter. 
    // So, we may release a 'slot' we never were able to claim
    CacheSemaphore.Release();
}

Do this:

if (await CacheSemaphore.WaitAsync(5000) == true)
{
   try {
      // ok to run the task
   }
   finally
   {
      CacheSemaphore.Release();
   }
}
else
{
  // not ok to run the task
}
Jussive answered 5/1, 2024 at 5:44 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.