7

Semaphores are a multi-threading locking mechanism that ensure that only a limited number of threads are running on a given resource. Mutexes are a special case where that limited number is one.

Asynchronous programming has a lot in common with (and sometimes to do with) multi-threaded programming, even though it's not inherently multi-threaded itself.

The following code creates ten tasks that simply wait for a second and log their start and end.

All of them are executed on only one thread (I assume a proper synchronization context maintenance is in place as is the case in WPF, for instance).

So even though there's only one thread we have "parallel" tasks and there can be use cases where one would want to limit access to a resource to only a few or one of those tasks. (For example, to limit parallel network requests.)

It appears there's need for an "async semaphore" - a concept that locks out not threads but asynchronous continuations.

I've implemented such a semaphore to check whether it really does make sense and to spell out what exactly I mean.

My question would be: Is this thing available already, ideally in the .NET framework itself? I couldn't find anything, although it seems to me that it should exist.

So here's the code (LINQPad share here):

 async void Main() { // Necessary in LINQPad to ensure a single thread. // Other environments such as WPF do this for you. SynchronizationContext.SetSynchronizationContext( new DispatcherSynchronizationContext()); var tasks = Enumerable.Range(1, 10).Select(SampleWork).ToArray(); await Task.WhenAll(tasks); "All done.".Dump(); } AsyncSemaphore commonSemaphore = new AsyncSemaphore(4); async Task SampleWork(Int32 i) { using (await commonSemaphore.Acquire()) { $"Beginning work #{i} {Thread.CurrentThread.ManagedThreadId}".Dump(); await Task.Delay(TimeSpan.FromSeconds(1)); $"Finished work #{i} {Thread.CurrentThread.ManagedThreadId}".Dump(); } } public class AsyncSemaphore { Int32 maxTasks; Int32 currentTasks; ReleasingDisposable release; Queue<TaskCompletionSource<Object>> continuations = new Queue<TaskCompletionSource<Object>>(); public AsyncSemaphore(Int32 maxTasks = 1) { this.maxTasks = maxTasks; release = new ReleasingDisposable(this); } public async Task<IDisposable> Acquire() { ++currentTasks; if (currentTasks > maxTasks) { var tcs = new TaskCompletionSource<Object>(); continuations.Enqueue(tcs); await tcs.Task; } return release; } void Release() { --currentTasks; if (continuations.Count > 0) { var tcs = continuations.Dequeue(); tcs.SetResult(null); } } class ReleasingDisposable : IDisposable { AsyncSemaphore self; public ReleasingDisposable(AsyncSemaphore self) => this.self = self; public void Dispose() => self.Release(); } } 

I get this output:

Beginning work #1 1 Beginning work #2 1 Beginning work #3 1 Beginning work #4 1 Finished work #4 1 Finished work #3 1 Finished work #2 1 Finished work #1 1 Beginning work #5 1 Beginning work #6 1 Beginning work #7 1 Beginning work #8 1 Finished work #5 1 Beginning work #9 1 Finished work #8 1 Finished work #7 1 Finished work #6 1 Beginning work #10 1 Finished work #9 1 Finished work #10 1 All done. 

So indeed, I have at most 4 tasks running, and all are running on the same thread.

6
  • 3
    Have you looked at SemaphoreSlim? It has a WaitOneAsync method. Commented Jun 24, 2017 at 17:41
  • @ScottChamberlain No, the SemaphoreSlim class is a semaphore for multiple threads. My question is a about an analogous single-threaded concept of semaphores. Commented Jun 24, 2017 at 22:02
  • 3
    SemaphoreSlim works just as well on a single thread. If the same thread calls WaitAsync multiple times in succession, it will keep decrementing the counter until it hits 0, then suspend execution until a Release is called. See this answer. Commented Jun 24, 2017 at 22:41
  • 2
    At a glance, yes. The main issue I can see is that SemaphoreSlim does not guarantee FIFO, although it might provide it when used on a single thread. Commented Jun 24, 2017 at 22:58
  • 1
    Checking the source it looks like internally it locks on all waits and releases then uses a linked list for tracking the waiters, so it should be FIFO ordering on the current implementation of .net. Commented Jun 25, 2017 at 0:22

1 Answer 1

4

So even though there's only one thread we have "parallel" tasks

I generally prefer the term "concurrent" just to avoid confusion with Parallel / Parallel LINQ.

My question would be: Is this thing available already, ideally in the .NET framework itself?

Yes. SemaphoreSlim is a semaphore that may be used synchronously or asynchronously.

I also have a full suite of asynchronous coordination primitives on NuGet, inspired by Stephen Toub's blog post series on the subject. My primitives are all sync-and-async compatible (and threadsafe), which is useful if, e.g., one user of a resource is synchronous but others are asynchronous.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.