149

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?

Blanthor
  • 2,568
  • 8
  • 48
  • 66
Mou
  • 15,673
  • 43
  • 156
  • 275
  • 11
    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. – Austin Salgat Jan 14 '18 at 16:53
  • 2
    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? – Shane Lu Aug 07 '20 at 02:44

3 Answers3

315

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.

dandiez
  • 3,177
  • 2
  • 5
  • 6
99

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.

LeopardSkinPillBoxHat
  • 28,915
  • 15
  • 75
  • 111
Servy
  • 202,030
  • 26
  • 332
  • 449
  • 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. – Mou Nov 18 '13 at 20:29
  • @Mou 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. – Servy Nov 18 '13 at 21:34
  • 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. thanks – Thomas Nov 19 '13 at 08:33
  • 4
    @Thomas 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. – Servy Nov 19 '13 at 14:56
  • 3
    @Servy 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. – Joseph Lennox Feb 17 '15 at 19:56
  • @JosephLennox As the comment indicates, the `Thread.Sleep` is just a placeholder for actual CPU bound work. – Servy Feb 17 '15 at 19:59
  • 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 SempaphoreSlim – BritishDeveloper May 21 '15 at 09:23
  • @BritishDeveloper 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. – Servy May 21 '15 at 13:38
  • 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? – BritishDeveloper May 21 '15 at 14:25
  • 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. – Servy May 21 '15 at 14:27
  • Couldn't he have just as easily put his `ss.Release()` after his `await Task.WhenAll`? – Grinn May 29 '19 at 21:25
  • @Grinn If they want the code to deadlock and never finish, they could do that. – Servy May 29 '19 at 21:35
  • @Servy Haha. Yes, you're right. How silly of me. He _probably_ doesn't want that. – Grinn May 30 '19 at 13:06
  • @Servy ~ 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`? – InteXX Nov 29 '19 at 19:17
  • @Servy ~ OK, I think I got it. I put the `Try/Finally` and `ss.Release()` call in the body of the `DoPollingThenWorkAsync()` function. Agreed? – InteXX Nov 29 '19 at 19:37
  • @InteXX 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. – Servy Dec 02 '19 at 00:12
  • 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? – InteXX Dec 02 '19 at 09:52
  • *10 active threads* <== this is misleading. When awaiting an asynchronous operation there is [no "active thread"](https://blog.stephencleary.com/2013/11/there-is-no-thread.html) waiting for the result. "Active waiting" is a contradiction by itself anyway. – Theodor Zoulias Dec 04 '19 at 03:06
  • *" ... 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. – InteXX Dec 04 '19 at 09:25
  • @InteXX "Perhaps create a function another level down" that would work. – Servy Dec 05 '19 at 03:35
  • @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. – Servy Dec 05 '19 at 03:36
  • 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](https://blog.stephencleary.com/2013/11/there-is-no-thread.html) article by Stephen Cleary for details. – Theodor Zoulias Dec 05 '19 at 05:06
  • @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*. – Servy Dec 06 '19 at 03:45
  • 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. – Theodor Zoulias Dec 06 '19 at 04:12
  • @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. – Servy Dec 07 '19 at 18:40
  • 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". – Theodor Zoulias Dec 07 '19 at 19:08
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.

andrew pate
  • 3,833
  • 36
  • 28
  • 8
    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. – ProgrammingLlama May 19 '18 at 09:23