3

I would like to ask simple question about code bellow:

static void Main(string[] args)
{
    MainAsync()
        //.Wait();
        .GetAwaiter().GetResult();
}

static async Task MainAsync()
{
    Console.WriteLine("Hello World!");

    Task<int> a = Calc(18000);
    Task<int> b = Calc(18000);
    Task<int> c = Calc(18000);

    await a;
    await b;
    await c;

    Console.WriteLine(a.Result);
}

static async Task<int> Calc(int a)
{
    //await Task.Delay(1);
    Console.WriteLine("Calc started");

    int result = 0;

    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }

    return result;
}

This example runs Calc functions in synchronous way. When the line //await Task.Delay(1); will be uncommented, the Calc functions will be executed in a parallel way.

The question is: Why, by adding simple await, the Calc function is then async? I know about async/await pair requirements. I'm asking about what it's really happening when simple await Delay is added at the beginning of a function. Whole Calc function is then recognized to be run in another thread, but why?

Edit 1:

When I added a thread checking to code:

static async Task<int> Calc(int a)
{
    await Task.Delay(1);
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

    int result = 0;

    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }

    return result;
}

it is possible to see (in console) different thread id's. If await Delay line is deleted, the thread id is always the same for all runs of Calc function. In my opinion it proves that code after await is (can be) runned in different threads. And it is the reason why code is faster (in my opinion of course).

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
user1885750
  • 101
  • 5
  • 4
    Adding `async` to a method declaration does not magically make it asynchronous, nor use separate threads. Without the `await Task.Delay(1);` statement, your method is 100% synchronous, and will thus run sequentially. However, with the task involved, you now (likely) involve the thread pool to run the continuation (the code after the delay). Additionally, when code in an `async` method reaches an `await task` where the task has not yet completed, it will queue up the rest of the method as a continuation, and then return, allowing all those 3 method calls to start like that. – Lasse V. Karlsen Jul 10 '20 at 11:45
  • 1
    Regarding the edit, Yes, in a console application specifically, the tasks will run on different threads. No, this is not the reason why they all start concurrently. The reason is that with `Task.Delay`, the execution returns to `MainAsync` before the task is complete, and without it it doesn't, like explained in [this better version](https://stackoverflow.com/a/62836559/11683) of [my original answer](https://stackoverflow.com/a/62833061/11683). Same would happen if the tasks did not run on separate threads. – GSerg Jul 10 '20 at 17:43

4 Answers4

8

It's important to understand how async methods work.

First, they start running synchronously, on the same thread, just like every other method. Without that await Task.Delay(1) line, the compiler will have warned you that the method would be completely synchronous. The async keyword doesn't, by itself, make your method asynchronous. It just enables the use of await.

The magic happens at the first await that acts on an incomplete Task. At that point the method returns. It returns a Task that you can use to check when the rest of the method has completed.

So when you have await Task.Delay(1) there, the method returns at that line, allowing your MainAsync method to move to the next line and start the next call to Calc.

How the continuation of Calc runs (everything after await Task.Delay(1)) depends on if there is a "synchronization context". In ASP.NET (not Core) or a UI application, for example, the synchronization context controls how the continuations run and they would run one after the other. In a UI app, it would be on the same thread it started from. In ASP.NET, it may be a different thread, but still one after the other. So in either case, you would not see any parallelism.

However, because this is a console app, which does not have a synchronization context, the continuations happen on any ThreadPool thread as soon as the Task from Task.Delay(1) completes. That means each continuation can happen in parallel.

Also worth noting: starting with C# 7.1 you can make your Main method async, eliminating the need for your MainAsync method:

static async Task Main(string[] args)
Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • This example and your answer must be added to the official documentation! – variable Jul 31 '21 at 19:17
  • @variable this is a really good answer, but it's value is only educational. Don't take it as a lesson about how to implement parallelism. The correct way to offload work to a background thread is the `Task.Run` method. Put everything that you want to offload inside the `Task.Run` delegate, and then `await` the returned `Task` at the right moment. Injecting `await Task.Delay(1)` or `await Task.Yield()` in async methods with the intention to parallelize the rest of the method, is a dirty hack at best. – Theodor Zoulias Aug 01 '21 at 00:25
  • Theodor - thinking about it as a hack will only work for console app. Not windows forms app. Right? And this is a genuine parallelism solution provided that Task.Delay is replaced with a genuine async method like HTTP POST call. – variable Aug 01 '21 at 13:48
  • @variable *"provided that Task.Delay is replaced with a genuine async method"* - So I think we all agree that you should not use `Task.Delay(1)` for the purpose of introducing parallelism. But even if you use some other async method there, consider the future maintainability of your code. If your app relies on the parallelism, then that's not obvious to the next person maintaining your code. However, if you use `Task.Run` instead, then anyone looking at your code knows that there is parallelism happening, which is important since all kinds of weird things can happen with parallelism. – Gabriel Luci Aug 01 '21 at 20:59
  • Gabriel - suppose I have a for loop that iterates over a datatable containing 10000 rows, is it safe to use Task.Run at each iteration to make the http request? Just concerned whether that will consume all cpu threads and harm the machine? And also I have read that Task.Run is for cpu bound tasks whereas http request is io bound so is it OK to use task.run? – variable Aug 02 '21 at 02:48
  • Remember that parallel is different than asynchronous. Async lets you run other code while you *wait* for a response. Parallel means *running* two pieces of code at the same time, which can only be done with multiple threads. For iterating over multiple rows where you make an HTTP request for each one, making it async will allow you to move on to the next one while you wait for a response. So it would fire off all 10000 HTTP requests at once and process the responses after. That's async. The responses would only be processed in parallel if there is no synchronization context (a console app). – Gabriel Luci Aug 03 '21 at 14:51
  • In ASP.NET (not Core) or desktop app, where there is a synchroniztion context, the responses would be processed one after the other. Usually that's good enough. If you want it to process the responses in parallel, then you'd need to use `Task.Run`. But yes, it would be a bad idea to use `Task.Run` in a loop that run 10000 times. – Gabriel Luci Aug 03 '21 at 14:56
1

An async function returns the incomplete task to the caller at its first incomplete await. After that the await on the calling side will await that task to become complete.

Without the await Task.Delay(1), Calc() does not have any awaits of its own, so will only return to the caller when it runs to the end. At this point the returned Task is already complete, so the await on the calling site immediately uses the result without actually invoking the async machinery.

GSerg
  • 76,472
  • 17
  • 159
  • 346
1

layman's version....

nothing in the process is yielding CPU time back without 'delay' and so it doesn't give anything else CPU time, you are confusing this with multiple threaded code. "async and await" is not about multiple threaded but about using the CPU (thread/threads) when its doing non CPU work" aka writing to disk. Writing to disk does not need the thread (CPU). So when something is async, it can free the thread and be used for something else instead of waiting for non CPU (oi task) to complete.

@sunside is saying the same thing just more technically.

static async Task<int> Calc(int a)
{
    //faking a asynchronous .... this will give this thread to something else 
    // until done then return here...
    // does not make sense... as your making this take longer for no gain.
    await Task.Delay(1);


    Console.WriteLine("Calc started");

    int result = 0;
   
    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }

    return result;
}

vs

static async Task<int> Calc(int a)
{
    
    using (var reader = File.OpenText("Words.txt"))
    {
        //real asynchronous .... this will give this thread to something else 
        var fileText = await reader.ReadToEndAsync();
        // Do something with fileText...
    }
        
    Console.WriteLine("Calc started");

    int result = 0;
   
    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }

    return result;
}

the reason it looks like its in "parallel way" is that its just give the others tasks CPU time.

example; aka without delay

  • await a; do this (no actual aysnc work)
  • await b; do this (no actual aysnc work)
  • await c; do this (no actual aysnc work)

example 2;aka with delay

  • await a; start this then pause this (fake async), start b but come back and finish a
  • await b; start this then pause this (fake async), start c but come back and finish b
  • await c; start this then pause this (fake async), come back and finish c

what you should find is although more is started sooner, the overall time will be longer as it as to jump between tasks for no benefit with a faked asynchronous task. where as, if the await Task.Delay(1) was a real async function aka asynchronous in nature then the benefit would be it can start the other work using the thread which would of been blocked... while it waits for something which does not require the thread.

update silly code to show its slower... Make sure you are in "Release" mode you should always ignore the first run... these test are silly and you would need to use https://github.com/dotnet/BenchmarkDotNet to really see the diff

static void Main(string[] args)
{
    Console.WriteLine("Exmaple1 - no Delay, expecting it to be faster, shorter times on average");
    for (int i = 0; i < 10; i++)
    {
        Exmaple1().GetAwaiter().GetResult();
    }

    Console.WriteLine("Exmaple2- with Delay, expecting it to be slower,longer times on average");
    for (int i = 0; i < 10; i++)
    {
        Exmaple2().GetAwaiter().GetResult();
    }
}


static async Task Exmaple1()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Task<int> a = Calc1(18000); await a;
    Task<int> b = Calc1(18000); await b;
    Task<int> c = Calc1(18000); await c;
    stopwatch.Stop();
    Console.WriteLine("Time elapsed: {0}", stopwatch.Elapsed);
}

static async Task<int> Calc1(int a)
{
    int result = 0;
    for (int k = 0; k < a; ++k) { for (int l = 0; l < a; ++l) { result += l; } }
    return result;
}

static async Task Exmaple2()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Task<int> a = Calc2(18000); await a;
    Task<int> b = Calc2(18000); await b;
    Task<int> c = Calc2(18000); await c;
    stopwatch.Stop();
    Console.WriteLine("Time elapsed: {0}", stopwatch.Elapsed);
}

static async Task<int> Calc2(int a)
{
    await Task.Delay(1);
    int result = 0;
    for (int k = 0; k < a; ++k){for (int l = 0; l < a; ++l) { result += l; } }
    return result;
}
Seabizkit
  • 2,417
  • 2
  • 15
  • 32
  • The classic example of I/O work is making web requests. Writing to disk is not actually pure I/O, because of how the related async APIs are implemented (they are implemented [poorly](https://stackoverflow.com/questions/59043037/read-text-file-with-iasyncenumerable/60443393#60443393)). – Theodor Zoulias Jul 10 '20 at 12:08
  • @TheodorZoulias 100% but the idea is there.... basically something which is not context bound by the current thread. As i said i was trying to dumb it down.. as some were marking sunside answer down... which made no sense to me. i think there was miss match of understanding. I understand... I'm not asking the question... simply trying to give an simple example not super specific.. to lay down the foundation..of the point sunside was laying down... i was trying to highlight that without `Task.Delay(1);` you are simply writing an synchronous operation because it synchronous in nature, which... – Seabizkit Jul 10 '20 at 12:15
  • Would only starts to make more sense if you had an example of an operation which supports asynchronous in their nature. aka one could be writing to disk or reading from disk. there are many but was trying to relate to one which ppl can relate to.. – Seabizkit Jul 10 '20 at 12:17
  • @TheodorZoulias https://en.wikipedia.org/wiki/Input/output – Seabizkit Jul 10 '20 at 13:54
  • @Seabizkit Actually, the overall time with faked asynchronous tasks (await Task.Delay(1)) is 3 times shorter. This is the reason why I used parallel sentence, because the time observation, give me conclusion, that all code after await Task.Delay, was performed in different threads. I don't know if I am right in this conclusion? – user1885750 Jul 10 '20 at 14:20
  • @user1885750 i just wrote a test... which proves it its slower.... so i guess it all depend on how you write the code which you use to test, if you test code it flawed then the test is flawed. ill add the test code i to verify... this is not and exact science and you should use actual bench-marking tools but even in this crude test u can see its slower. – Seabizkit Jul 10 '20 at 14:48
  • @Seabizkit But there is a huge difference in your code in comparison to my. Your code: Task a = Calc2(18000); await a; Task b = Calc2(18000); await b; Task c = Calc2(18000); await c; my code is: Task a = Calc(18000); Task b = Calc(18000); Task c = Calc(18000); await a; await b; await c; I tested my code by using Stopwach (Release, Console Application, .NET Core) and it froved that code with await Task.Delay is much faster – user1885750 Jul 10 '20 at 15:29
  • @user1885750 i was also seeing what u were... but i know better in terms of how to test and what the code is actually doing so i know that it will be slower (in this case).... but to help with this.. you need to google "debug vs release mode in .net for performance" which will lead you to.. https://github.com/dotnet/BenchmarkDotNet. – Seabizkit Jul 10 '20 at 15:33
  • @Seabizkit I don't know what you exactly mean. Do you think I measured code in wrong way? It's no rocket science. Just use my code (not yours) and measure it. I obtained 0.5s without Delay and 0.28 with Delay (Console, .NET Core, Release). I am no idea why u obtained the opposite results. Maybe someone else will test code also. – user1885750 Jul 10 '20 at 15:44
  • mine and urs are the same thing basically...just shortened the read length and ran multiple times... never use the first results... (this is called warm-up or cold start) anyway you meant to take the average..., you are working with such a small about in it.. that its basically un-measurable..run the same test multiple times.. is the result ever the same? no... run the same code... multiple times not from cold start.. in release mode... and you will see that every-time if more than 100 calls the one without the delay will win. the faction of cpu time you are trying to measure is very very s – Seabizkit Jul 10 '20 at 16:45
  • ps this link is gold https://learn.microsoft.com/en-us/archive/blogs/benwilli/tasks-are-still-not-threads-and-async-is-not-parallel – Seabizkit Jul 10 '20 at 16:52
  • @Seabizkit But it is obvious. I give you average time of many measurements. I am implementing algorithms for many years and I have experience on time measurements. I measured execution time many times in one run of program. I also measured another problems/algorithms by using the same scenario (with tree Tasks and await Delay). Additionally , I measured this problem outside visual studio and always I have similar results. Maybe it is because of Console application. – user1885750 Jul 10 '20 at 17:41
0

By using an async/await pattern, you intend for your Calc method to run as a task:

Task<int> a = Calc(18000);

We need to establish that tasks are generally asynchronous in their nature, but not parallel - parallelism is a feature of threads. However, under the hood, some thread will be used to execute your tasks on. Depending on the context your running your code in, multiple tasks may or may not be executed in parallel or sequentially - but they will be (allowed to be) asynchronous.

One good way of internalizing this is picturing a teacher (the thread) answering question (the tasks) in class. A teacher will never be able to answer two different questions simultaneously - i.e. in parallel - but she will be able to answer questions of multiple students, and can also be interrupted with new questions in between.


Specifically, async/await is a cooperative multiprocessing feature (emphasis on "cooperative") where tasks only get to be scheduled onto a thread if that thread is free - and if some task is already running on that thread, it needs to manually give way. (Whether and how many threads are available for execution is, in turn, dependent on the environment your code is executing in.)

When running Task.Delay(1) the code announces that it is intending to sleep, which signals to the scheduler that another task may execute, thereby enabling asynchronicity. The way it's used in your code it is, in essence, a slightly worse version of Task.Yield (you can find more details about that in the comments below).

Generally speaking, whenever you await, the method currently being executed is "returned" from, marking the code after it as a continuation. This continuation will only be executed when the task scheduler selects the current task for execution again. This may happen if no other task is currently executing and waiting to be executed - e.g. because they all yielded or await something "long-running".

In your example, the Calc method yields due to the Task.Delay and execution returns to the Main method. This, in turn enters the next Calc method and the pattern repeats. As was established in other answers, these continuations may or may not execute on different threads, depending on the environment - without a synchronization context (e.g. in a console application), it may happen. To be clear, this is neither a feature of Task.Delay nor async/await, but of the configuration your code executes in. If you require parallelism, use proper threads or ensure that your tasks are started such that they encourage use of multiple threads.

In another note: Whenever you intend to run synchronous code in an asynchronous manner, use Task.Run() to execute it. This will make sure it doesn't get in your way too much by always using a background thread. This SO answer on LongRunning tasks might be insightful.

sunside
  • 8,069
  • 9
  • 51
  • 74
  • Async/await is not concerned with threads and has nothing to do with cooperative multiprocessing and thread scheduling. Please see https://stackoverflow.com/q/17661428/11683 or https://blog.stephencleary.com/2013/11/there-is-no-thread.html. – GSerg Jul 10 '20 at 11:15
  • Every task is executed on a thread. These threads are managed by the task scheduler. – sunside Jul 10 '20 at 11:16
  • im not sure why this was marked down.... i upped... although a bit technical it not wrong. – Seabizkit Jul 10 '20 at 11:17
  • 1
    @Seabizkit It was voted down because it's very wrong. Async/await is not about threads, and saying that it is is very misleading. The OP has a console application that has no synchronization context, so the tasks there have no obligation to run on the same thread, so they may very easily end up on different threads, one for each `a`, `b` and `c`, which already disproves the "if that thread is free" portion, because there *is* an available thread each time, yet it's not used. – GSerg Jul 10 '20 at 11:22
  • @Seabizkit If the same code was run in a Winforms application, where the synchronization context is bound to the UI thread, then `a`, `b` and `c` would all run on the same thread, but they all would be in flight simultaneously, not in order of completion, which disproves it from the other side: there *isn't* an available thread, yet all three tasks are started simultaneously. – GSerg Jul 10 '20 at 11:22
  • @GSerg Whatever thread is used, it needs to be free for a task to be scheduled on it. Whether or not there's multiple threads available is an entirely different question, as is which thread is used to execute a continuation. If it wouldn't be about threads, then running `Thread.Sleep(int32.MaxValue)` in a Task would be a perfectly sane thing to do ... and yet it isn't. – sunside Jul 10 '20 at 11:24
  • 1
    The `Task.Delay(1)` can be configured to capture the current synchronization context or not. The `Task.Yield` is not configurable. It always captures the context. This is one reason why these two are not equivalent. The other reason is that the `Task.Yield` when awaited will always respond to [`IsCompleted`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.yieldawaitable.yieldawaiter.iscompleted) with `false`. This is not guaranteed for `Task.Delay(1)`, and awaiting it could be a synchronous operation (potentially). – Theodor Zoulias Jul 10 '20 at 12:18
  • 1
    I think the answer could be improved by mentioning that the behavior would be different if run on a UI thread. – JonasH Jul 10 '20 at 14:27
  • 2
    Everything about this is wrong. "tasks are asynchronous in their nature" `Calc` *isn't* asynchronous. It clearly should be, but because it was implemented improperly, it's not. "but not parallel" If it *were* asynchronous, what the OP is doing would run it in parallel, regardless of its internals. "some thread will be used to execute your tasks on" No, lots of tasks represent work *not done by any thread at all*. It's just (hopefully) asynchronous, somehow. – Servy Jul 10 '20 at 15:52
  • 1
    "Depending on the context your running your code in, multiple tasks may or may not be executed in parallel or sequentially" Only if those methods are scheduling on to run on the current synchronization context. They should only be doing that for work that is not long running though. "async/await is a cooperative multiprocessing feature (emphasis on "cooperative") where tasks only get to be scheduled onto a thread" No. Tasks are a representation of work done in the future, not a mechanism of scheduling work on threads. Scheduling work on threads is separate, and not inherent to tasks. – Servy Jul 10 '20 at 15:52
  • 2
    "When running Task.Delay(1) the code announces that it is intending to sleep" No. Running that just produces a task that will be complete in a millisecond. It does nothing to the execution of the current method at all. "Note that whenever you await, the method currently being executed is 'returned' from" No, it's the first await of a task that's not already complete. – Servy Jul 10 '20 at 15:52
  • 1
    "This continuation will only be executed when the task scheduler selects the current task for execution again" *If* you configure the await to use the current synchronization context, and if there is one, then the continuation is scheduled on that context. That's not the same as what you said. – Servy Jul 10 '20 at 15:52