8

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.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
user1451111
  • 1,735
  • 3
  • 18
  • 30
  • 1
    The whole point of 'timing out a request' is that some X cannot wait for N seconds that Y needs and throws timeout error. In our case X is client that is using the api. In Asp.Net request won't be timed out automatically, it'll be put in a queue and wait for first available thread to serve it. But the fact that all threads are blocked for N seconds already proves the inefficiency of using sync calls for blocking operations – Fabjan Sep 18 '18 at 06:13
  • Just a reminder that "ASP.NET worker threads" are plain thread pool threads. – Paulo Morgado Sep 18 '18 at 22:22
  • first this is quite trick to show due to knowing how many threads the machine is capable of this is machine hardware and os dependent, in terms of identifying max through-put. I would say you have chosen one of the hardest ways to show this as you making it specific about web end point which means you have to test from this prospective. To do this you need to construct a benchmark which hits an endpoint with x request per second, you would need to variances an async and sync version both doing x request per second, you would then need two build something to monitor threads and cpu time. – Seabizkit Aug 06 '20 at 16:26
  • read this to help with building a suitable test https://labs.criteo.com/2018/10/net-threadpool-starvation-and-how-queuing-makes-it-worse/ and look at this https://stackoverflow.com/questions/20109849/how-to-track-net-thread-pool-usage – Seabizkit Aug 06 '20 at 16:48
  • this is also a gd read https://stackoverflow.com/questions/28690815/iocp-threads-clarification – Seabizkit Aug 06 '20 at 16:52
  • Does this answer your question? [Shouldn't lesser number of threads be used if I use async?](https://stackoverflow.com/questions/23259763/shouldnt-lesser-number-of-threads-be-used-if-i-use-async) – Seabizkit Aug 08 '20 at 11:49
  • https://stackoverflow.com/questions/23259763/shouldnt-lesser-number-of-threads-be-used-if-i-use-async found the perfect example... – Seabizkit Aug 08 '20 at 11:50

2 Answers2

-1

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
mtanksl
  • 592
  • 6
  • 9
-1

Here they do a gd job at exampling and its a good example.

Shouldn't lesser number of threads be used if I use async?

Seabizkit
  • 2,417
  • 2
  • 15
  • 32