Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SemaphoreSlim.WaitAsync continuation code

My understanding of the await keyword was that the code following the await qualified statement is running as the continuation of that statement once it is complete.

Hence the following two versions should produce the same output:

    public static Task Run(SemaphoreSlim sem)
    {
        TraceThreadCount();
        return sem.WaitAsync().ContinueWith(t =>
        {
            TraceThreadCount();
            sem.Release();
        });
    }

    public static async Task RunAsync(SemaphoreSlim sem)
    {
        TraceThreadCount();
        await sem.WaitAsync();
        TraceThreadCount();
        sem.Release();
    }

But they do not!

Here is the complete program:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace CDE
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                var sem = new SemaphoreSlim(10);
                var task = Run(sem);

                Trace("About to wait for Run.");

                task.Wait();

                Trace("--------------------------------------------------");
                task = RunAsync(sem);

                Trace("About to wait for RunAsync.");

                task.Wait();
            }
            catch (Exception exc)
            {
                Console.WriteLine(exc.Message);
            }
            Trace("Press any key ...");
            Console.ReadKey();
        }

        public static Task Run(SemaphoreSlim sem)
        {
            TraceThreadCount();
            return sem.WaitAsync().ContinueWith(t =>
            {
                TraceThreadCount();
                sem.Release();
            });
        }

        public static async Task RunAsync(SemaphoreSlim sem)
        {
            TraceThreadCount();
            await sem.WaitAsync();
            TraceThreadCount();
            sem.Release();
        }

        private static void Trace(string fmt, params object[] args)
        {
            var str = string.Format(fmt, args);
            Console.WriteLine("[{0}] {1}", Thread.CurrentThread.ManagedThreadId, str);
        }
        private static void TraceThreadCount()
        {
            int workerThreads;
            int completionPortThreads;
            ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
            Trace("Available thread count: worker = {0}, completion port = {1}", workerThreads, completionPortThreads);
        }
    }
}

Here is the output:

[9] Available thread count: worker = 1023, completion port = 1000
[9] About to wait for Run.
[6] Available thread count: worker = 1021, completion port = 1000
[9] --------------------------------------------------
[9] Available thread count: worker = 1023, completion port = 1000
[9] Available thread count: worker = 1023, completion port = 1000
[9] About to wait for RunAsync.
[9] Press any key ...

What am I missing?

like image 838
mark Avatar asked Sep 03 '14 22:09

mark


1 Answers

async-await optimizes for when the task you're awaiting on has already completed (which is the case when you have a semaphore set to 10 with only 1 thread using it). In that case the thread just carries on synchronously.

You can see that by adding an actual asynchronous operation to RunAsync and see how it changes the thread pool threads being used (which would be the behavior when your semaphore is empty and the caller actually needs to wait asynchronously):

public static async Task RunAsync(SemaphoreSlim sem)
{
    TraceThreadCount();
    await Task.Delay(1000);
    await sem.WaitAsync();
    TraceThreadCount();
    sem.Release();
}

You can also make this change to Run and have it execute the continuation synchronously and get the same results as in your RunAsync (thread count wise):

public static Task Run(SemaphoreSlim sem)
{
    TraceThreadCount();
    return sem.WaitAsync().ContinueWith(t =>
    {
        TraceThreadCount();
        sem.Release();
    }, TaskContinuationOptions.ExecuteSynchronously);
}

Output:

[1] Available thread count: worker = 1023, completion port = 1000  
[1] Available thread count: worker = 1023, completion port = 1000  
[1] About to wait for Run.  
[1] --------------------------------------------------  
[1] Available thread count: worker = 1023, completion port = 1000  
[1] Available thread count: worker = 1023, completion port = 1000  
[1] About to wait for RunAsync.  
[1] Press any key ...  

Important Note: When it's said that async-await acts as a continuation it's more of an analogy. There are several critical difference between these concepts, especially regarding SynchronizationContexts. async-await automagically preserves the current context (unless you specify ConfigureAwait(false)) so you can use it safely in environments where that matters (UI, ASP.Net, etc.). More about synchronization contexts here.

Also, await Task.Delay(1000); may be replaced with await Task.Yield(); to illustrate that the timing is irrelevant and just the fact that the method waits asynchronously matters. Task.Yield() is often useful in unit tests of asynchronous code.

like image 101
i3arnon Avatar answered Nov 15 '22 06:11

i3arnon