-1

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.

I created an example. I see with my own eyes through the windows task manager that the while (i < int.MaxValue) operation is processed first, taking up the entire processor resource, although I first launched the DownloadFile. And only then, when the processor is released, I see that the download files is in progress. On my machine, the example runs ~54 seconds.

Question: how could I first run the DownloadFile asynchronously so that the threads do not idle uselessly, but can do while (i < int.MaxValue)?

using System.Net;

string PathProject = Directory.GetParent(Directory.GetCurrentDirectory()).Parent.Parent.Parent.FullName;

//Create folder 1 in the project folder
DirectoryInfo Path = new DirectoryInfo($"{PathProject}\\1");

int Iterations = Environment.ProcessorCount * 3;
string file = "https://s182vla.storage.yandex.net/rdisk/82b08d86b9920a5e889c6947e4221eb1350374db8d799ee9161395f7195b0b0e/62f75403/geIEA69cusBRNOpxmtup5BdJ7AbRoezTJE9GH4TIzcUe-Cp7uoav-lLks4AknK2SfU_yxi16QmxiuZOGFm-hLQ==?uid=0&filename=004%20-%2002%20Lesnik.mp3&disposition=attachment&hash=e0E3gNC19eqNvFi1rXJjnP1y8SAS38sn5%2ByGEWhnzE5cwAGsEnlbazlMDWSjXpyvq/J6bpmRyOJonT3VoXnDag%3D%3D&limit=0&content_type=audio%2Fmpeg&owner_uid=160716081&fsize=3862987&hid=98984d857027117759bc5ce6092eaa6a&media_type=audio&tknv=v2&rtoken=k9xogU6296eg&force_default=no&ycrid=na-2bc914314062204f1cbf810798018afd-downloader16e&ts=5e61a6daac6c0&s=eef8b08190dc7b22befd6bad89e1393b394869a1668d9b8af3730cce4774e8ad&pb=U2FsdGVkX1__q3AvjJzgzWG4wVR80Oh8XMl-0Dlfyu9FhqAYQVVkoBV0dtBmajpmOkCXKUXPbREOS-MZCxMNu2rkAkKq_n-AXcZ85svtSFs";

List<Task> tasks = new List<Task>();

void MyMethod1(int i)
{
    WebClient client = new WebClient();
    client.DownloadFile(file, $"{Path}\\{i}.mp3");
}

void MyMethod2()
{
    int i = 0;
    while (i < int.MaxValue)
    {
        i++;
    }
}

DateTime dateTimeStart = DateTime.Now;

for (int i = 0; i < Iterations; i++)
{
    int j = i;
    tasks.Add(Task.Run(() => MyMethod1(j)));
}

for (int i = 0; i < Iterations; i++)
{
    tasks.Add(Task.Run(() => { MyMethod2(); MyMethod2(); }));
}

Task.WaitAll(tasks.ToArray());

Console.WriteLine(DateTime.Now - dateTimeStart);

while (true)
{
    Thread.Sleep(100);

    if (Path.GetFiles().Length == Iterations)
    {
        Thread.Sleep(1000);

        foreach (FileInfo f in Path.GetFiles())
        {
            f.Delete();
        }

        return;
    }
}
  • 1
    If you want to do async HTTP, [HttpClient](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-6.0) is preferred over WebRequest for [various reasons](https://stackoverflow.com/questions/27793761/httpclient-vs-httpwebrequest-for-better-performance-security-and-less-connectio). For one thing, it is much easier to write asynchronous code. There are [lots of examples](https://stackoverflow.com/search?q=httpclient+%5Bc%23%5D) on Stackoverflow. – John Wu Aug 12 '22 at 02:42
  • I have a goal to understand why it is needed with a simple example with a network request. –  Aug 12 '22 at 02:46
  • If you don't care about creating many many many threads, which is what people try to avoid by asynchronous programming, then you should probably avoid the `Task.Run` method that relies on the `ThreadPool`. Instead you can just create all the threads you need immediately, with the [`Thread`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread) constructor. Or use the `Task.Factory.StartNew`+`LongRunning` combination, as shown [here](https://stackoverflow.com/questions/71111345/application-doevents-while-waiting-another-thread/71111774#71111774). – Theodor Zoulias Aug 12 '22 at 03:00
  • Threads are expensive and resource-hungry (at least on Windows). So waisting a thread that is waiting most of the time is not good. I once had to maintain a library for accessing some devices via IP. Customers complained that with about 200 devices, the library did not work any more. Turned out that it created a read thread per device. Switching to async/await solved the issue. – Klaus Gütter Aug 12 '22 at 04:45
  • @KlausGütter, Great example. Thank you. Let's imagine that these devices are on the Internet and these are some sites and we make requests to them. Can you provide code that proves that the task/thread option works one way and worse, while async/await works differently and better? That's what I'm trying to do, but I'm not getting anything. I just don't understand how exactly to do it. –  Aug 12 '22 at 05:14

4 Answers4

2

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)

tymtam
  • 31,798
  • 8
  • 86
  • 126
  • Thank you. I read the theory, I'm trying to write a primitive example from which it will be seen / became. Benefits of async\await. The simplest real example. –  Aug 12 '22 at 05:21
  • The above is not theory, but OK I'll write up a toy example – tymtam Aug 12 '22 at 08:55
2
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.

Jeremy Lakeman
  • 9,515
  • 25
  • 29
  • Thank you. Can you give a code with a thousand long Internet requests - with tasks and with async\await? And from this example we will see that the program runs faster with async\await. –  Aug 12 '22 at 05:40
  • It's not that difficult to build a synthetic benchmark https://artemmikulich.medium.com/async-vs-sync-benchmark-net-f1e752a57755 But that doesn't prove that the program runs "faster". – Jeremy Lakeman Aug 12 '22 at 06:54
  • See also https://stackoverflow.com/a/62919974/4139809 – Jeremy Lakeman Aug 12 '22 at 07:00
  • @JeremyLakeman, That is, I need to use a typic method with the async suffix? Can't you just add async await to the old GetResponse method? Usage async await is reduced to using typical framework methods with the suffix async? –  Aug 13 '22 at 03:12
  • Framework methods that return an awaitable should end with `Async`. It's a convention that MS follow. – Jeremy Lakeman Aug 14 '22 at 06:37
1

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.

It's kind of recursive, but async is best used whenever there's something asynchronous. In other words, anything where the CPU would be wasted if it had to just spin (or block) while waiting for the operation to complete. Operations that are naturally asynchronous are generally I/O-based (as you mention, DB and other network calls, as well as file I/O), but they can be more arbitrary events, too (e.g., timers). Anything where there isn't actual code to run to get the response.

Many people write that async\await is also needed so as not to block the main thread.

At a higher level, there are two primary benefits to async/await, depending on what kind of code you're talking about:

  • On the server side (e.g., web apps), async/await provides scalability by using fewer threads per request.
  • On the client side (e.g., UI apps), async/await provides responsiveness by keeping the UI thread free to respond to user input.

Developers tend to emphasize one or the other depending on the kind of work they normally do. So if you see an async article talking about "not blocking the main thread", they're talking about UI apps specifically.

And here it is completely unclear to me why it should be blocked. Don't block without async\await, just create a task.

That works just fine for many situations. But it doesn't work well in others.

E.g., it would be a bad idea to just Task.Run onto a background thread in a web app. The primary benefit of async in a web app is to provide scalability by using fewer threads per request, so using Task.Run does not provide any benefits at all (in fact, scalability is reduced). So, the idea of "use Task.Run instead of async/await" cannot be adopted as a universal principle.

The other problem is in resource-constrained environments, such as mobile devices. You can only have so many threads there before you start running into other problems.

But if you're talking Desktop apps (e.g., WPF and friends), then sure, you can use async/await to free up the UI thread, or you can use Task.Run to free up the UI thread. They both achieve the same goal.


Question: how could I first run the DownloadFile asynchronously so that the threads do not idle uselessly, but can do while (i < int.MaxValue)?

There's nothing in your code that is asynchronous at all. So really, you're dealing with multithreading/parallelism. In general, I recommend using higher-level constructs such as Parallel for parallelism rather than Task.Run.

But regardless of the API used, the underlying problem is that you're kicking off Environment.ProcessorCount * 6 threads. You'll want to ensure that your thread pool is ready for that many threads by calling ThreadPool.SetMinThreads with the workerThreads set to a high enough number.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thank you. This is the question. First there is a cycle that needs a processor, then a download that does not need a processor. If the download of files does not use the processor and the loop uses how to first start the download and then the loop so that they happen together and do not interfere with each other because they use different resources. I thought asynchrony would help with this. I want to simulate a situation without abstract delays and with a real I\O ops without async wait and with it, but without using typical asynchronous methods. I hope my goal is clear. –  Aug 13 '22 at 03:27
  • Use asynchronous methods (not `Task.Run`) for the I/O, and you'll see the difference. – Stephen Cleary Aug 13 '22 at 04:24
  • Well, these methods need someone to write. Or do you mean that the developers of the framework control the asynchrony mechanism of i\o operations and we just have to use their methods marked async? Just take what you already have and use keywords? I wanted to create my own method that work differently without async and it would be related to i\o operations. –  Aug 13 '22 at 05:03
  • In other words, how to write your own asynchronous method using the DownloadFile method? For this to be equivalent to DownloadFileAsync. –  Aug 13 '22 at 06:43
  • >99% of asynchronous methods just call other asynchronous methods and use `await`. E.g., you can write an asynchronous `DowbloadFileAsybc` by using asynchronous HTTP APIs and asynchronous file APIs. For the lowest-level asynchronous methods, you would use `TaskCompletionSourxe`. But you don't do that when first learning `async`. And you can't make a truly asynchronous `DownloadFileAsync` using `DownloadFile`. – Stephen Cleary Aug 13 '22 at 11:45
-1

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}");
}
tymtam
  • 31,798
  • 8
  • 86
  • 126