2

In programs utilizing async-await, my understanding is like this:

  • an async method that IS NOT awaited will run in the background (?) and rest of the code will continue to execute before this non-awaited method finishes
  • an async method that IS awaited will wait until the method finishes before moving on to the next lines of code

The application below was written by me to check if the above statements are correct.

using System;
using System.Threading.Tasks;

namespace ConsoleApp3
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            DoJob();

            var z = 3;

            Console.ReadLine();
        }

        static async Task DoJob()
        {
            var work1 = new WorkClass();
            var work2 = new WorkClass();

            while (true)
            {
                await work1.DoWork(500);
                await work2.DoWork(1500);
            }
        }
    }

    public class WorkClass
    {
        public async Task DoWork(int delayMs)
        {
            var x = 1;

            await Task.Delay(delayMs);

            var y = 2;
        }
    }
}

Here are some of my observations:

  • The DoJob(); call is not awaited. However, the debugger shows me that the code inside of DoJob is being executed, just as if it was a normal non-async method.
  • When code execution gets to await work1.DoWork(500);, I would think "OK, so maybe now the DoJob method will be left and var z = 3; will be executed? After all, 'await' should leave the method." In reality, it just goes into DoWork and doesn't leave DoJob - var z = 3; is still not executed.
  • Finally, when execution reaches await Task.Delay(delayMs);, DoJob is left, and the var z = 3; is reached. After that, code after the Delay is executed.

The things that I don't understand:

  • Why does await Task.Delay(delayMs); leave the DoJob method, but await work1.DoWork(500); does not?
  • I see that DoJob is executing normally. I thought it would be done in the background (maybe by one of the thread pool threads?). Looks like it could block the thread if it was some long-running method, am I right?
Boann
  • 48,794
  • 16
  • 117
  • 146
Loreno
  • 668
  • 8
  • 26
  • 3
    `async`/`await` does not create any new threads - it's only if the awaited method itself **explicitly** creates a task (say using `Task.Run`) that a thread is created (or utilized from the thread-pool). See https://blog.stephencleary.com/2012/02/async-and-await.html – Enigmativity Jun 23 '19 at 12:52
  • It leaves when it gets to the code that can actually run asynchronously and bubbles up. So when you see an await it has to deal with that and then it can leave. And similarly it will continue where it left off from the lowest level working back to the top possibly leaving again and going through the same upper level awaits. – juharr Jun 23 '19 at 12:54
  • Your understanding is wrong, `async/await` do not create tasks by themselves, and until you create a task all your `async/await` calls will be like normal calls, here in your case `Task.Delay` creates a task and waits for it because of your `await` and here the control given back to the caller `Main` which does not wait and continues normally. – muaz Jun 23 '19 at 13:03
  • 2
    You should have read and researched the __Warning__ before posting here. Do not ignore compiler warnings, resolve them. – H H Jun 23 '19 at 19:13
  • @HenkHolterman I know that compiler suggests to await everyhing, but that's actually another thing that is not so clear to me. I'll post another question later probably. – Loreno Jun 24 '19 at 07:15

6 Answers6

9

Why does await Task.Delay(delayMs); leave the DoJob method, but await work1.DoWork(500); does not?

Because this code:

await work1.DoWork(500);

is the same as this code:

var task = work1.DoWork(500);
await task;

So your code is calling the method first, and then awaiting the returned task. It's common to talk about await as "awaiting method calls", but that's not what actually happens - technically, the method call is done first (synchronously), and then the returned task is awaited.

I see that DoJob is executing normally. I thought it would be done in the background (maybe by one of the thread pool threads?).

No; with true asynchronous operations, there is no thread that is blocked on that operation.

Looks like it could block the thread if it was some long-running method, am I right?

Yes.

my understanding is like this

I recommend reading my async intro for a better mental framework. In summary:

  1. async enables the await keyword. It also generates a state machine that handles creating the Task return value and stuff like that.
  2. await operates on an "awaitable" (usually a task). First, it checks to see if it's already complete; if it is, the async method continues executing synchronously.
  3. If the awaitable is not already complete, then await (by default) captures its context and schedules the continuation of the async method to run on that context when the awaitable completes.
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    Your example is really good! My understanding became better. When some Method1 calls awaitable Method2 (with await), it will always go into Method2. Method1 can be "left" only after Method2 returns an incomplete task. So, if Method2 is not able to return incomplete Task, the program will work as if it was synchronous one. Am I correct? The confusion is that we could be using a library with many async methods, but we don't know if they are really async! They could just return Task.CompletedTask at the end. We can't know if the method we invoke will be a blocking one or not! Is't that a problem? – Loreno Jun 24 '19 at 07:28
  • Yes, and yes. It is *possible* for a method with an asynchronous signature to actually behave synchronously. This is rare, but it does happen. I always recommend documenting somewhere that the method runs synchronously. – Stephen Cleary Jun 24 '19 at 11:47
5

The compiler splits the code in an async method in chunks. 1 before the first await and 1 between each await and 1 after the last await.

The execution will return to the caller at the first non completed awaiter or the end of the method.

This method will only return a completed Task after fully executed:

async Task M1() => await Task.CompletedTask;

This method will only return an incomplete Task that will complete when the Task returned by Task.Dealy(1000) is completed:

async Task M2() => await Task.Delay(1000);

Here's a small example:

static async Task Main(string[] args)
{
    var t = TwoAwaits();
    Console.WriteLine("Execution returned to main");
    await t;
}
private static async Task TwoAwaits()
{
    Console.WriteLine("Before awaits");
    await Task.CompletedTask;
    Console.WriteLine("Between awaits #1");
    await Task.Delay(1000);
    Console.WriteLine("Between awaits #2");
    await Task.Delay(1000);
    Console.WriteLine("After awaits");
}
/*
Before awaits
Between awaits #1
Execution returned to main
Between awaits #2
After awaits
*/
Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
  • Yeah, I understand that when you await something, first thing that is checked is if the task is completed already. If it is, we move on. In case of Task.Delay(1000) we leave the method, because task is not completed. However, if I create async method that does some really heavy calculations (and it doesn't do it in new threads, etc.) my method will execute as normal - it will not leave the caller method on the await. Instead, it will be executing as normal, sequentially. That's my point - when you run awaitable method you can't really know if this method is truly async (an will return) or not. – Loreno Jun 24 '19 at 07:23
2

Let's look at the four possibilities:

(1)

void Main()
{
    Console.WriteLine($"Main 0 - {Thread.CurrentThread.ManagedThreadId}");
    DoJob();
    Console.WriteLine($"Main 1 - {Thread.CurrentThread.ManagedThreadId}");
}

public static async Task DoJob()
{
    Console.WriteLine($"DoJob 0 - {Thread.CurrentThread.ManagedThreadId}");
    Thread.Sleep(2000);
    Console.WriteLine($"DoJob 1 - {Thread.CurrentThread.ManagedThreadId}");
}

This outputs:

Main 0 - 14
DoJob 0 - 14
DoJob 1 - 14
Main 1 - 14

It has a 2 second pause after DoJob 0.

(2)

async Task Main()
{
    Console.WriteLine($"Main 0 - {Thread.CurrentThread.ManagedThreadId}");
    await DoJob();
    Console.WriteLine($"Main 1 - {Thread.CurrentThread.ManagedThreadId}");
}

public static async Task DoJob()
{
    Console.WriteLine($"DoJob 0 - {Thread.CurrentThread.ManagedThreadId}");
    Thread.Sleep(2000);
    Console.WriteLine($"DoJob 1 - {Thread.CurrentThread.ManagedThreadId}");
}

Again this outputs:

Main 0 - 14
DoJob 0 - 14
DoJob 1 - 14
Main 1 - 14

(3)

async Task Main()
{
    Console.WriteLine($"Main 0 - {Thread.CurrentThread.ManagedThreadId}");
    await DoJob();
    Console.WriteLine($"Main 1 - {Thread.CurrentThread.ManagedThreadId}");
}

public static Task DoJob()
{
    return Task.Run(() =>
    {
        Console.WriteLine($"DoJob 0 - {Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(2000);
        Console.WriteLine($"DoJob 1 - {Thread.CurrentThread.ManagedThreadId}");
    });
}

This has different output because it has changed thread:

Main 0 - 15
DoJob 0 - 13
DoJob 1 - 13
Main 1 - 13

And finally:

async Task Main()
{
    Console.WriteLine($"Main 0 - {Thread.CurrentThread.ManagedThreadId}");
    DoJob();
    Console.WriteLine($"Main 1 - {Thread.CurrentThread.ManagedThreadId}");
}

public static Task DoJob()
{
    return Task.Run(() =>
    {
        Console.WriteLine($"DoJob 0 - {Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(2000);
        Console.WriteLine($"DoJob 1 - {Thread.CurrentThread.ManagedThreadId}");
    });
}

This has a different output again:

Main 0 - 13
Main 1 - 13
DoJob 0 - 12
DoJob 1 - 12

In this last case it is not waiting for DoJob because DoJob is running on a different thread.

So if you follow the logic here the issue is that async/await doesn't create (or use) a different thread. The method called must do that.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • 1
    I feel like this might confuse the OP more, as this explicitly asks for another Thread with `Task.Run`, when the OP is using `Task.Delay` (and hence the outputs will be different, Main0 1 > DoJob0 1 > Main1 1 > DoJob1 4 in my PC) – Camilo Terevinto Jun 23 '19 at 13:07
  • `In this last case it is not waiting for DoJob because DoJob is running on a different thread.`- no, in the last case it is not waiting for DoJob because DoJob is not awaited. DoJob also runs on a separate thread in the second to last example, but it is awaited there. In the first case the caller would not wait for DoJob either if DoJob used `Task.Delay` instead of `Sleep` which blocks the caller too. – GSerg Jun 23 '19 at 13:09
2

Before Async & Await, there was two type of methods. Those who returned the result directly, and those who received a callback function as a parameter. On the latter, the method was invoked in the same thread syncronously and returned no value, and later, on the same or different thread your callback function would have been called with the result. Historically all I/O (disk, network, even memory) worked with callbacks (actually: interrupts) but medium to high level languages like C# would mask all that internally so end users don't need to learn/write low level code.

This worked pretty well up to a point, except this optimization wasted some physical resources. For example Node.js outperformed several other languages/server platforms by their limitation that forces the developers to use the callback model instead of the 'managed' mode.

This pushed C# and other languages to go back to the callback model, but the code readability really suffered (code callback spaguetti). So Async and Await was introduced.

Async and await let's you write in the 'callback model' with 'managed' syntax. All callbacks are handled by the compiler.

Each time you write 'await' in an async method, your method is actually split into two methods connected by a callback.

Now, you can write an async method that does regular sync code, without awaits, nor thread switch or I/O. That 'async' method will actually run synchronously. So, it is actually the same to await method1() or call without await. Why? because your async call is not awaiting anything, so your async code is still one piece of continous code.

If inside your method you await one, two or more different methods, then your method will be split into one, two or more pieces. And only the first piece will be guaranteed to be run synchronously. All the other pieces will run on other thread depending on the code that you are awaiting.

TL;DR;

  • Async/Await method is does not guarantees multi-threading or parallel processing. That will actually depend on the payload (the called async method). For example, http downloads will tipically be paralellized if you manage your awaits because those are functions that are mostly waiters of an external response. On the other side, heavy CPU processing, like compressing a file, will require other form of cpu/thread management not provided by async/await.

  • If you do not await an async method, your code will surely run synchronously up to the first await of the called method, given it has one. But later on, it may or not run sync.

Gerardo Grignoli
  • 14,058
  • 7
  • 57
  • 68
1

why does await Task.Delay(delayMs); leave the DoJob method, but await work1.DoWork(500); does not?

Because, up and until there is an actual asynchronous call, it's still in the same context. If DoWork was just:

public async Task DoWork(int delayMs)
{
    var x = 1;
    var y = 2;

    return Task.CompletedTask;
}

there would be no need for a continuation and hence, you would debug all the way without "jumping" back to the orignal await call.

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
  • Ok, it looks to me that calling awaitable methods without await is actually a bit risky. You don't really know when execution will jump out of the called method - it could be in the middle of it (if there was some await) or actually the method could be executed fully (because it could be like your example). Another thing I see is that execution of async method will "jump out" of this method only if it awaits something TRULY async (new thread, I/O operation, network operation). Am I right? – Loreno Jun 23 '19 at 15:09
  • 1
    @Loreno You shouldn't really care when/if it "jumps out", that's only a debugger experience. Not awaiting asynchronous methods works when you need actual background tasks or you want the task to do its job while you do something else. As for the second part of your comment, it doesn't need to be truly async, just getting another Thread (with `Task.Run`) is enough, although, again, you should never care about that. The work started and/or finished, you will get its value when you `await` it or call `.Result` on it (warning: synchronous) – Camilo Terevinto Jun 23 '19 at 15:31
0

Here is how your application could be remodeled if you were forced to avoid async/await for some reason. See how complex it gets to replicate the logic inside the loop. Async/await is really a gift from the heavens!

using System;
using System.Threading.Tasks;

namespace ConsoleApp3
{
    class Program
    {
        static Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            DoJob();

            var z = 3;

            Console.ReadLine();
            return Task.CompletedTask;
        }

        static Task DoJob()
        {
            var work1 = new WorkClass();
            var work2 = new WorkClass();

            var tcs = new TaskCompletionSource<bool>();
            Loop();
            return tcs.Task;

            void Loop()
            {
                work1.DoWork(500).ContinueWith(t1 =>
                {
                    if (t1.IsFaulted) { tcs.SetException(t1.Exception); return; }
                    work2.DoWork(1500).ContinueWith(t2 =>
                    {
                        if (t2.IsFaulted) { tcs.SetException(t2.Exception); return; }
                        if (true) { Loop(); } else { tcs.SetResult(true); }
                        // The 'if (true)' corresponds to the 'while (true)'
                        // of the original code.
                    });
                });
            }
        }
    }

    public class WorkClass
    {
        public Task DoWork(int delayMs)
        {
            var x = 1;

            int y;

            return Task.Delay(delayMs).ContinueWith(t =>
            {
                if (t.IsFaulted) throw t.Exception;
                y = 2;
            });
        }
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104