1

i feel pretty confused by the output i'm getting out of what i believe is purely async program. As you can observe there are no obvious anti patterns (i hope) and blocking calls.

slowURL throttles server response for 10 seconds. I did confirm by running calls to the local server with 10 second timeout that FetchSlowAsync method call effectively blocks the main thread for 10 seconds when running the code in console.

I expected that TaskScheduler would schedule the calls not in a sequential manner but by always randomly determining the method call order. Alas the output is always deterministic.

FetchSlowAsync start
FetchSlowAsync got data!
FetchAsync start
FetchAsync got data!
FetchBingAsync start
FetchBingAsync got data!
All done!

My question is: what prompts FetchSlowAsync to block rather than TaskScheduler to perform a context switch to another async method and get back to it when it's done?

And the next question that follows the former: why all the methods in async Main are executed in the same order as they are being called given the async execution model is concurrent?

using static System.Console;
using System.Net.Http;
using System.Threading.Tasks;

class Start
{
    const string serviceURL = "https://google.com";
    const string slowDown = "http://slowwly.robertomurray.co.uk/delay/10000/url/";
    const string slowURL = slowDown + serviceURL;
    const string OkURL = serviceURL;

    static async Task FetchSlowAsync()
    {
        await Console.Out.WriteLineAsync("FetchSlowAsync start");
        await new HttpClient().GetStringAsync(slowURL); //BLOCKS MAIN THREAD FOR 10 seconds        
        await Console.Out.WriteLineAsync("FetchSlowAsync got data!");
    }

    static async Task FetchAsync()
    {
        await Console.Out.WriteLineAsync("FetchAsync start");
        await new HttpClient().GetStringAsync(OkURL);
        await Console.Out.WriteLineAsync("FetchAsync got data!");        
    }

    static async Task FetchBingAsync()
    {
        await Console.Out.WriteLineAsync("FetchBingAsync start");
        await new HttpClient().GetStringAsync("https://bing.com");
        await Console.Out.WriteLineAsync("FetchBingAsync got data!");
    }

    static async Task Main()
    {
        await FetchSlowAsync();
        await FetchBingAsync();
        await FetchAsync();

        await System.Console.Out.WriteLineAsync("All done!");
    }
}

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
alexeyhaidamaka
  • 168
  • 1
  • 9
  • Would you mind if I refactored your code to not include local methods? There's no need for them here, and they're just added complexity. – Jon Skeet May 25 '19 at 18:05
  • Next, it sounds like you're expecting these to happen in parallel. Why? You're always awaiting whatever asynchronous task you've just started. You're awaiting the task returned by `FetchSlowAsync()` before you call `FetchBingAsync()`, for example. – Jon Skeet May 25 '19 at 18:07
  • (I can write that up in more detail as an answer, if you like. But it really depends on whether/why you expected things to happen in parallel.) – Jon Skeet May 25 '19 at 18:09
  • @JonSkeet sure Jon, i don't mind. That snippet was added for demo purposes only since my expectations of execution heavily differ from the output. – alexeyhaidamaka May 25 '19 at 18:09
  • @JonSkeet I think you already answered this well enough – Camilo Terevinto May 25 '19 at 18:17
  • @CamiloTerevinto pardon, but my intention wasn't to ask how to parallelise the code but rather to figure why the scheduler doesn't context switch on slow running IO. – alexeyhaidamaka May 25 '19 at 18:24
  • Why would it? How would it know it's "slow running"? How do you decide when it's slow and when it isn't? – Camilo Terevinto May 25 '19 at 18:26
  • @CamiloTerevinto i leave that decision to the runtime itself Take a look at a similar implementation in Go https://pastebin.com/Uzs4Hd4m – alexeyhaidamaka May 25 '19 at 18:27
  • That looks *far* more similar to what Jon Skeet answered than to your code – Camilo Terevinto May 25 '19 at 18:29
  • @CamiloTerevinto well, the default `async/await` doc makes the impression that all thing are taken care of by the CLR itself while in Go i should state things explicitly. I didn't realise topmost `await` call will basically block until async task returns. – alexeyhaidamaka May 25 '19 at 18:40
  • 1
    Well, perhaps you should read the documentation on async, await and TPL in general. [From the docs](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/await) "The await operator is applied to a task in an asynchronous method to insert a suspension point in the execution of the method until the awaited task completes" – Camilo Terevinto May 25 '19 at 18:43

1 Answers1

8

Currently, you're waiting for the task returned from FetchSlowAsync() to complete before moving on to call FetchBingAsync etc. You're doing that by awaiting the tasks, here:

await FetchSlowAsync();
await FetchBingAsync();
await FetchAsync();

If you don't want to wait for the slow fetch to complete before you start the Bing fetch, etc, you can remember the tasks returned by them, and await them later:

static async Task Main()
{
    // Start all three tasks
    Task t1 = FetchSlowAsync();
    Task t2 = FetchBingAsync();
    Task t3 = FetchAsync();

    // Then wait for them all to finish
    await Task.WhenAll(t1, t2, t3);

    await Console.Out.WriteLineAsync("All done!");
}

Now you get the kind of output I think you're expecting. For example, here's one run on my machine:

FetchSlowAsync start
FetchBingAsync start
FetchAsync start
FetchAsync got data!
FetchBingAsync got data!
FetchSlowAsync got data!
All done!
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • i guess i was expecting a bit more from C#'s async than it can actually give. I thought that if `await` exceeds IL instruction or time threshold it switches to the main thread and it will pick up the next async method to execute (in case of my code). – alexeyhaidamaka May 25 '19 at 18:22
  • 1
    @alexeyhaidamaka: I would say that's not "expecting more" but "expecting very different" - and I think it would be much worse, by being *far* less predictable. If an `await` operator could just get bored and proceed with the next statement, what would you expect it to do if you were using the result of the `await` operator? It's really important to understand what async/await actually do, because then you can decide on the behavior you want. – Jon Skeet May 25 '19 at 18:58