Reputation: 7049
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.
Upvotes: 7
Views: 1673
Reputation: 457157
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.
Upvotes: 4