Semaphores for single-threaded asynchronous programming in the asynchronous wait style

Semaphores are a multithreaded locking mechanism that ensures that only a limited number of threads are running on a given resource. Mutexes are a special case where this limited number is one.

Asynchronous programming has a lot in common with (and sometimes is related to) multithreading programming, although it is not multithreaded in itself.

The following code creates ten tasks that just wait a second and register their start and end.

They all only execute one thread at a time (I'm assuming proper maintenance of the synchronization context is done, as in WPF, for example).

So, although there is only one thread, we have "parallel" tasks, and there may be times when you need to restrict resource access to only a few or one of these tasks. (For example, to limit concurrent network requests.)

It seems that an "asynchronous semaphore" requires a concept that blocks not threads, but asynchronous continuations.

I implemented a semaphore like this to check if it really makes sense and to indicate what exactly I mean.

My question is, will this thing already be available, 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 am getting 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, I have no more than 4 running tasks running and they all work in the same thread.

+3


source to share


1 answer


So, although there is only one thread, we have "parallel" tasks

I usually prefer the term "concurrent" to avoid confusion with Parallel

/ Parallel LINQ.

My question is, will this thing already be available, ideally in the .NET framework itself?



Yes. SemaphoreSlim

is a semaphore that can be used synchronously or asynchronously.

I also have a complete set of asynchronous coordination primitives on NuGet, inspired by the Stephen Tobe Blog Series . My primitives are synchronous and asynchronous compatible (and thread safe), which is useful if, for example, one user of a resource is synchronous and others are asynchronous.

+3


source







All Articles