Home > other >  Async\await again. Example with network requests
Async\await again. Example with network requests

Time:08-12

I completely don't understand the applied meaning of async\await.

I just started learning async\await and I know that there are already a huge number of topics. If I understand correctly, then async\await is not needed anywhere else except for operations with a long wait in a thread, if this is not related to a long calculation. For example, database response, network request, file handling. Many people write that async\await is also needed so as not to block the main thread. And here it is completely unclear to me why it should be blocked. Don't block without async\await, just create a task. So I'm trying to create a code that will wait a long time for a response from the network.

In my case, this code fails with an exception after 20 seconds:

Console.WriteLine(WebRequest.Create("https://192.168.1.1").GetResponse().ContentLength)

Suppose there are 32 threads on my machine, I want to write a program so that 32 tasks execute the above code and at the same time 32 tasks perform a conditionally long operation. I wrote this code without using async await, all 64 tasks started and there are no problems. What am I doing wrong?

Can someone give a practical, easy to understand example without a lot of horror where I just can't live without async\await and my threads will be idle for nothing?

using System.Net;

void MyMethod(int i)
{
    try
    {
        Console.WriteLine($"Start {i}");
        Console.WriteLine(WebRequest.Create("https://192.168.1.1").GetResponse().ContentLength);
    }
    catch
    {
        Console.WriteLine($"End {i}");
    }
}

for (int i = 0; i < Environment.ProcessorCount; i  )
{
    int j = i;
    Task.Run(() => MyMethod(j));
}

for (int i = 0; i < Environment.ProcessorCount; i  )
{
    int j = i;
    Task.Run(() => { Console.WriteLine($"Long operation {j}"); Thread.Sleep(int.MaxValue); });
}

Thread.Sleep(int.MaxValue);

CodePudding user response:

If there are 2 web servers that talk to a database and they run on 2 machines with the same spec the web server with async code will be able to handle more concurrent requests.

The following is from 2014's Async Programming : Introduction to Async/Await on ASP.NET

Why Not Increase the Thread Pool Size?

At this point, a question is always asked: Why not just increase the size of the thread pool? The answer is twofold: Asynchronous code scales both further and faster than blocking thread pool threads.

Asynchronous code can scale further than blocking threads because it uses much less memory; every thread pool thread on a modern OS has a 1MB stack, plus an unpageable kernel stack. That doesn’t sound like a lot until you start getting a whole lot of threads on your server. In contrast, the memory overhead for an asynchronous operation is much smaller. So, a request with an asynchronous operation has much less memory pressure than a request with a blocked thread. Asynchronous code allows you to use more of your memory for other things (caching, for example).

Asynchronous code can scale faster than blocking threads because the thread pool has a limited injection rate. As of this writing, the rate is one thread every two seconds. This injection rate limit is a good thing; it avoids constant thread construction and destruction. However, consider what happens when a sudden flood of requests comes in. Synchronous code can easily get bogged down as the requests use up all available threads and the remaining requests have to wait for the thread pool to inject new threads. On the other hand, asynchronous code doesn’t need a limit like this; it’s “always on,” so to speak. Asynchronous code is more responsive to sudden swings in request volume.

(These days threads are added added every 0.5 second)

CodePudding user response:

WebRequest.Create("https://192.168.1.1").GetResponse()

At some point the above code will probably hit the OS method recv(). The OS will suspend your thread until data becomes available. The state of your function, in CPU registers and the thread stack, will be preserved by the OS while the thread is suspended. In the meantime, this thread can't be used for anything else.

If you start that method via Task.Run(), then your method will consume a thread from a thread pool that has been prepared for you by the runtime. Since these threads aren't used for anything else, your program can continue handling other requests on other threads. However, creating a large number of OS threads has significant overheads.

Every OS thread must have some memory reserved for its stack, and the OS must use some memory to store the full state of the CPU for any suspended thread. Switching threads can have a significant performance cost. For maximum performance, you want to keep a small number of threads busy. Rather than having a large number of suspended threads which the OS must keep swapping in and out of each CPU core.

When you use async & await, the C# compiler will transform your method into a coroutine. Ensuring that any state your program needs to remember is no longer stored in CPU registers or on the OS thread stack. Instead all of that state will be stored in heap memory while your task is suspended. When your task is suspended and resumed, only the data which you actually need will be loaded & stored, rather than the entire CPU state.

If you change your code to use .GetResponseAsync(), the runtime will call an OS method that supports overlapped I/O. While your task is suspended, no OS thread will be busy. When data is available, the runtime will continue to execute your task on a thread from the thread pool.

Is this going to impact the program you are writing today? Will you be able to tell the difference? Not until the CPU starts to become the bottleneck. When you are attempting to scale your program to thousands of concurrent requests.

If you are writing new code, look for the Async version of any I/O method. Sprinkle async & await around. It doesn't cost you anything.

CodePudding user response:

It's not web requests but here's a toy example:

Test:

n:    1 await: 00:00:00.1373839 sleep: 00:00:00.1195186
n:   10 await: 00:00:00.1290465 sleep: 00:00:00.1086578
n:  100 await: 00:00:00.1101379 sleep: 00:00:00.6517959
n:  300 await: 00:00:00.1207069 sleep: 00:00:02.0564836
n:  500 await: 00:00:00.1211736 sleep: 00:00:02.2742309
n: 1000 await: 00:00:00.1571661 sleep: 00:00:05.3987737

Code:


using System.Diagnostics;

foreach( var n in new []{1, 10, 100, 300, 500, 1000})
{
   var sw = Stopwatch.StartNew();
   var tasks = Enumerable.Range(0,n)
      .Select( i => Task.Run( async () => 
      {
         await Task.Delay(TimeSpan.FromMilliseconds(100));
      }));

   await Task.WhenAll(tasks);
   var tAwait = sw.Elapsed;

   sw = Stopwatch.StartNew();
   var tasks2 = Enumerable.Range(0,n)
      .Select( i => Task.Run( () => 
      {
         Thread.Sleep(TimeSpan.FromMilliseconds(100));
      }));

   await Task.WhenAll(tasks2);
   var tSleep = sw.Elapsed;
   Console.WriteLine($"n: {n,4} await: {tAwait} sleep: {tSleep}");
}
  • Related