how to exhaust ASP.NET worker threads to show importance of async await pattern

Viewed 674

I am trying to demonstrate to a junior the importance of asynchronous programming, using async and await. For the purpose I have created an ASP.NET Web API project with one controller and two GET actions. One GET action is synchronous and the other is asynchronous.

I want to demonstrate that, in case of synchronous blocking i/o call, all of the available ASP.NET worker threads are waiting and doing nothing useful and in the meantime when some more requests arrive they will time out as all the available threads are waiting for i/o threads to complete.

The problem is that that my below code snippet partly conveys the point. it works as intended in case of asynchronous calls but not for synchronous calls. If I uncomment the commented out code lines, it doesn't happen and ASP.NET runtime can cope with many more threads. The code snippet is below:

public class TestController : ApiController
{        
    // -> Uncommenting the below method proves my point of scalability <-
    //public async Task<string> Get()
    //{
    //    CodeHolder obj = new CodeHolder();
    //    return await obj.AsyncData();
    //}
    // -> Uncommenting the below method doesn't enforce time outs, rather waits <-
    public string Get()
    {
        CodeHolder obj = new CodeHolder();
        return obj.SyncData();
    }        
}
class CodeHolder
{
    public string SyncData()
    {
        Task.Delay(10000).Wait();
        return $"I am returned from Sync after waiting for 10 second at {DateTime.Now.ToString("HH:mm:ss:fffffff")}";
    }
    public async Task<string> AsyncData()
    {
        await System.Threading.Tasks.Task.Delay(10000);
        return $"I am returned from Async after semi-waiting for 10 second at {DateTime.Now.ToString("HH:mm:ss:fffffff")}";
    }
}

Although the point I was trying to propose, gets conveyed as the synchronous calls take ages to complete but I am wondering why the requests are being kept in queue instead of time outs. I am using JMeter for sending 250 concurrent HTTP requests to my Web API service but they never time out rather they keep waiting and complete, although a very large delay (~250 seconds).

By the way, in async version, all of the responses are returned in around 10 seconds.

2 Answers

You could show that the sync version uses more threads from the threadpool than the async one.

This is what I came up with to demonstrate, the web controller:

[Route("api/v1/[controller]")]
public class ThreadController : Controller
{
    private static readonly object locker = new object();

    private static HashSet<int> syncIds = new HashSet<int>();

    [HttpGet("sync")]
    public ActionResult<string> GetSync()
    {
        var id = Thread.CurrentThread.ManagedThreadId;

        lock (locker)
        {
            syncIds.Add(id);
        }

        Task.Delay(10000).Wait();

        return $"I am returned from Sync after waiting for 10 second at {DateTime.Now.ToString("HH:mm:ss:fffffff")}";
    }

    [HttpGet("sync/count")]
    public ActionResult<int> CountSync()
    {
        lock (locker)
        {
            int count = syncIds.Count;

            syncIds.Clear();

            return count;
        }
    }

    private static HashSet<int> asyncIds = new HashSet<int>();

    [HttpGet("async")]
    public async Task<ActionResult<string>> GetAsync()
    {
        var id = Thread.CurrentThread.ManagedThreadId;

        lock (locker)
        {
            asyncIds.Add(id);
        }

        await Task.Delay(10000);

        return $"I am returned from Async after waiting for 10 second at {DateTime.Now.ToString("HH:mm:ss:fffffff")}";
    }

    [HttpGet("async/count")]
    public ActionResult<int> CountAsync()
    {
        lock (locker)
        {
            int count = asyncIds.Count;

            asyncIds.Clear();

            return count;
        }
    }
}

A console application to simulate the requests (I gave 150ms delay to let some time to reach the await block):

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var http = new HttpClient() )
            {
                var stopwatch = Stopwatch.StartNew();

                var sync = new List<Task>();

                for (int i = 0; i < 20; i++)
                {
                    sync.Add(http.GetAsync("http://localhost:5000/api/v1/thread/sync") );

                    Thread.Sleep(150);
                }

                Task.WaitAll(sync.ToArray() );

                stopwatch.Stop();

                Console.WriteLine("Sync used " + http.GetAsync("http://localhost:5000/api/v1/thread/sync/count").Result.Content.ReadAsStringAsync().Result + " threads in " + stopwatch.ElapsedMilliseconds + "ms");

                stopwatch.Restart();

                var async = new List<Task>();

                for (int i = 0; i < 20; i++)
                {
                    async.Add(http.GetAsync("http://localhost:5000/api/v1/thread/async") );

                    Thread.Sleep(150);
                }

                Task.WaitAll(async.ToArray() );

                stopwatch.Stop();

                Console.WriteLine("Async used " + http.GetAsync("http://localhost:5000/api/v1/thread/async/count").Result.Content.ReadAsStringAsync().Result + " threads in " + stopwatch.ElapsedMilliseconds + "ms");
            }

            Console.ReadLine();
        }
    }
}

The results I got:

first run:

Sync used 19 threads in 22412ms
Async used 8 threads in 12911ms

second run:

Sync used 18 threads in 21083ms
Async used 10 threads in 12921ms

third run:

Sync used 20 threads in 13578ms
Async used 10 threads in 12899ms

fourth run:

Sync used 18 threads in 21018ms
Async used 5 threads in 12889ms
Related