0

BlobService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AllAboutAsyncProgramming.services
{
    public class BlobService
    {
        public async Task<string> GetBlobAsync()
        {
            Console.WriteLine($"GetBlobAsync - {Environment.CurrentManagedThreadId}");

            HttpClient client = new HttpClient();

            var result = await client.GetAsync("https://www.google.com");

            Console.WriteLine($"GetBlobAsync - {Environment.CurrentManagedThreadId}");

            return result.ToString();
        }
    }
}

Program.cs

using AllAboutAsyncProgramming.services;

class Program
{
    public static async Task Main(string[] args)
    {
        BlobService blobService = new BlobService();
        
        Console.WriteLine($"Main - {Environment.CurrentManagedThreadId}");

        var x = await blobService.GetBlobAsync();

        Console.WriteLine($"Main - {Environment.CurrentManagedThreadId}");
    }
}

Result

example - 1

Main - 1
GetBlobAsync - 1
GetBlobAsync - 6
Main - 6

example - 2 (another run)

Main - 1
GetBlobAsync - 1
GetBlobAsync - 9
Main - 9

Result is always looks like this. first two lines of output get same thread id and bottom two lines of output always get same thread id. (as shown in results section)

When it execute this line of code --> var x = await blobService.GetBlobAsync(); isn't it GetBlobAsync method should be run on separate thread? But it prints main threads id. after the await call inside GetBlobAsync method --> await client.GetAsync("https://www.google.com"); it runs on a separate thread.

I'm confused with the execution flow. How it works? Isn't it should be run on a separate thread when we await something?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Kasun
  • 672
  • 8
  • 18
  • 4
    One of the main points of tasks and async/await is that you don't need to worry about implementation details. The system will decide when code needs to be run on a different thread. There is not a 1:1 correspondence between tasks and threads. If you're really interested in the details, you ought to do some reading on the subject because it's not a small topic. – jmcilhinney Apr 22 '23 at 03:10
  • 10
    I describe how the `async` and `await` keywords work [here](https://blog.stephencleary.com/2012/02/async-and-await.html). For truly asynchronous operations, [there is no thread](https://blog.stephencleary.com/2013/11/there-is-no-thread.html). – Stephen Cleary Apr 22 '23 at 03:16
  • 2
    Possibly related: [How Async and Await works](https://stackoverflow.com/questions/22349210/how-async-and-await-works). – Theodor Zoulias Apr 22 '23 at 03:37
  • 4
    Take @StephenCleary's advice and read his blog. Many of us who hang out there got our understanding from his writing – Flydog57 Apr 22 '23 at 06:08

2 Answers2

2

You should understand that calling an async method that returns Task or Task<T> is not different than calling a regular method. The real magic is going to happen when it hits await inside of the async method.

IMHO, a verbose illustration would be helpful for you. Let's call main thread [M] , a thread in the thread pool [T], and IOCP thread [I]

class Program
{
    public static async Task Main(string[] args)
    {
        // 1. 
        BlobService blobService = new BlobService();
        
        // 2. 
        Console.WriteLine($"Main - {Environment.CurrentManagedThreadId}");
        
        // 3. 
        var x = await blobService.GetBlobAsync();

        // 9. 
        Console.WriteLine($"Main - {Environment.CurrentManagedThreadId}");
    }
}

public async Task<string> GetBlobAsync()
{
    // 4. 
    Console.WriteLine($"GetBlobAsync - {Environment.CurrentManagedThreadId}");

    // 5. 
    HttpClient client = new HttpClient();

    // 6.
    //   6-1~6-5. 
    var result = await client.GetAsync("https://www.google.com");

    // 7. 
    Console.WriteLine($"GetBlobAsync - {Environment.CurrentManagedThreadId}");

    // 8. 
    return result.ToString();
}
  1. [M] instantiates BlobService.

  2. [M] synchronously runs Console.WriteLine.

  3. [M] synchronously runs GetBlowAsync.

  4. [M] synchronously runs Console.WriteLine.

  5. [M] instantiates HttpClient.

  6. [M] synchronously runs HttpClient.GetAsync; This is technically asynchronous operation in the end, but I choose to say synchronously because [M] will still execute a part of code lines in GetAsync. However it will eventually become free within a few microseconds or so the moment the Task<T> is returned. [M] can run another task in the work queue from now on. And the true asynchrony is achieved in these steps.

    6-1. Network I/O is happening under the hood.

    6-2. When it's done in kernel level, it propagates the result and eventually an overlapped message will pop up. If you want to see more detailed illustrations, please read there is no thread

    6-3. [I] gets the signal, notifying the task has been done.

    6-4. [T] takes the continuation task and runs the rest of the code only if there's no synchronization context or there's explicit ConfigureAwait(false);

    6-5. If synchronization context exists, the continuation will not start on [T], instead it will start on the caller thread like [M] in this case.

  7. [T] synchronously runs Console.WriteLine.

  8. [T] synchronously runs ToString().

  9. [T] synchronously runs Console.WriteLine.

hina10531
  • 3,938
  • 4
  • 40
  • 59
  • I'd like to know, why do you say that "if `synchronization context` exists, the continuation will not start on [T]..." Because it always capture the `synchronization context` right?. So there should be a `synchronization context` right? – Kasun May 22 '23 at 02:09
  • 1
    `It always captures the synchronization context` -> this is not true. That depends. `Console` application doesn't have the context unless you made one. – hina10531 May 22 '23 at 02:13
2

Try to follow these rules

  • Whenever you call an async method you start executing it Synchronously
  • Before you can evaluate the expression await something(), you have to evaluate the expression something()
  • If the await operator is applied to an incomplete task, the async method early returns (Synchronously!) to the caller an incomplete Task (representing the fact that the async method is still not completed). This Task will be completed when the async method ends (So the Task is reflecting the async method exeution status)
  • Whenever the incomplete task you were awaiting against passes to a complete status, the async method will be resumed, and it will execute whatever comes after the await.
  • If the async method returns a value, this is set to the .Result property of the Task early returned to the caller before. That is exactly the value returned from the await operator

Where?

The last but one point omits where the continuation (that is the part of the async method that comes after the await) will be executed. The idea is that an async method should "emulate" a synchronous method. So

The continuation will be executed in the same context there was before the await

In detail:

  • If a Synchronization Context is set before the await statement (SynchronizationContext.Current != default), the continutation will be executed in it (something like scBeforeAwait.Post(_ => continuation(), null))
  • Otherwise, if a non default TaskScheduler is set before the await statement (TaskScheduler.Current != TaskScheduler.Default), the continuation runs there. (Imagine in as Task.Factory.StartNew(() => continuation(), default, .., taskSchedulerBeforeAwait).
  • Otherwise the continuation runs on the ThreadPool

In your example

  • Before we can await blobService.GetBlobAsync() we have to evaluate blobService.GetBlobAsync();
  • We are calling an async method. We start executing it Synchronously (so we remain in the same thread)
  • Before we can evaluate await client.GetAsync("https://www.google.com") we have to evaluate client.GetAsync("https://www.google.com");
  • The task returned from client.GetAsync("https://www.google.com") is not yet completed, so we are awaiting an incompleted Task. The async method early returns back to the caller another incomplete Task.
  • The caller (the main method) is awaiting an incompleted Task, so it early returns an incompleted Task to the caller (The c# Compiler secretly adds a Main().Wait() in order to synchronously wait the Task returned from the Main method, so that the application does not terminate here). The application now is completely idle.
  • Whenever the page is ready the async method resumes and we start executing whatever comes next. Because at before the await we didn't have any SynchronizationContext nor TaskScheduler, the continuation runs on the Thread Pool (Thread with Id 6 in you case). The async method completes, and set the status of the Task returned before to the main as "completed". That triggers also the main method to resume.
  • Because also the main method wants to resume on the Thread Pool, the runtime makes an optimization and chooses to run the continuation "inline", that is on the same thread of whatever completed the Task (Thread 6).
  • The main method resumes on the same Thread