3

I has spend a lot of time to understand async programming principles. But one thing is still unclear. I was confused by this code :

    static async Task Method()
    {
        Console.WriteLine($"Method entered.");

        await Task.Delay(1000);
        Console.WriteLine($"Await 1 finished.");

        await Task.Delay(1000);
        Console.WriteLine($"Await 2 finished");
    }

    static int Main(string[] args)
    {
        Console.WriteLine($"Main started.");

        return AsyncContext.Run(() => MainAsync(args));
    }

    static async Task<int> MainAsync(string[] args)
    {
        var t = Method();
        Console.WriteLine("Thread starting sleep.");
        Thread.Sleep(10000);
        Console.WriteLine("Thread stopped sleeping");
        Console.WriteLine(t.IsCompleted ? "Method completed" : "Method not completed");
        await t;
        return 0;
    }

Result :

Main started.
Method entered.
Thread starting sleep.
Thread stopped sleeping
Method not completed
Await 1 finished.
Await 2 finished

As I understand while Main thread is sleeping IO-bound operations from Method should be executed (cause Task.Delay emulate IO) and interrupt Main thread sequentially to continue execute Method code. So I expect to see:

Main started.
Method entered.
Thread starting sleep.
Await 1 finished.
Await 2 finished
Thread stopped sleeping
Method completed

I know that by Thread.Sleep I am making to stop Main thread. But as I understand Method() should not need thread because it consists of IO-bound operations. Can anybody explain where I am misunderstanding it?

AsynContext that I am using is (here).

YV17
  • 359
  • 1
  • 2
  • 7
  • 1
    _"But as I understand Method() should not need thread because it consists of IO-bound operations"_ - no it doesn't. `Task.Delay()` has nothing to do with IOCP. http://stackoverflow.com/questions/32008662/reusable-test-code-that-waits-for-io –  Apr 18 '17 at 10:02
  • @MickyD But Task.Delay() is simulating IO async operation – YV17 Apr 18 '17 at 10:06
  • I suggest you read that link again. Though `Task.Delay()` simulates an asynchoronous operation, not all asynchonous operations are "IO-bound" and `Task.Delay()` certainly isn't IO-bound –  Apr 18 '17 at 10:08
  • Every Async call needn't be IO bound, which is though the main purpose, here Task.Delay does represent the true Async call, which blocks the continuation, while control is ceded back to the caller, which blocks it and thus continuation also cannot proceed – Mrinal Kamboj Apr 18 '17 at 12:23

3 Answers3

3

By default "await" captures the current synchronization context and spawns the continuation in that original context. If the context is undefined, continuation is spawned in the default thread pool (TaskScheduler.Default).

I'm not familiar with AsyncContext, but it probably spawns MainAsync under some well synchronization context and since Thread.Sleep blocks the thread that occupies that context, the continuation of "await" will wait until the context is freed.

This is not a strange phenomenon, you can reproduce it without the AsyncContext class. Try to run the same code in a Windows forms application and you will see. Windows forms have it's own synchronization context that guards against unsynchronized Control manipulation.

To overcome this, you could tell "await" to not capture the synchronization context by using the ConfigureAwait(false) method.

static async Task Method()
        {
            Console.WriteLine($"Method entered.");

            await Task.Delay(1000).ConfigureAwait(false);
            Console.WriteLine($"Await 1 finished.");

            await Task.Delay(1000).ConfigureAwait(false);
            Console.WriteLine($"Await 2 finished");
        }

await won't try to spawn continuation in the existing context, rather it would spawn it in a thread pool task.

areller
  • 4,800
  • 9
  • 29
  • 57
  • 1
    This is correct and would help in achieving the result that you need as you explicitly configure the await to suggest that Continuation context can be ignored – Mrinal Kamboj Apr 18 '17 at 11:53
1

Why your code is behaving correctly as expected ?

On using the AsyncContext.Run, you are providing an explicit context for Console application which otherwise have NULL Synchronization Context, now when you execute the following lines of code in MainAsync:

var t = Method();
Console.WriteLine("Thread starting sleep.");
Thread.Sleep(10000);

Then Method() starts executing, where on encountering the statement:

await Task.Delay(1000);

It cedes the control back to the caller, where you block the context by making the Thread Sleep for 10s Thread.Sleep(10000);, so now before this sleep is over the Continuation cannot take place in the Async method, since it waits for the Continuation context to be available, moment its free, then it starts executing the Continuation, but by that time it also complete the remaining statements in the MainAsync, which seems to be prioritized and response is as expected, it awaits only in the very end, in fact checking the Task status for any logic like t.IsCompleted is more of a code smell, better is only await t, which waits for the Task completion

There are two ways to get the behavior that you expect

  1. As shown by @Arik, configure both await using ConfigureAwait(false), what does this means, simple that for running the Async continuation it doesn't need the original context and that will continue as true Async operation, hence will provide the result as you expect. Most of the libraries wit Async functions especially IO based, implement ConfigureAwait(false).
  2. Make the call from the Main as return MainAsync(args).Result;, this will ensure standard behavior of the Console applications, which means NULL Synchronization Context, which means Async doesn't care about continuation on any existing Context, it goes on in background even when you are making the Thread sleep, since it doesn't expect that context and the result would be same as you expect

    Main started.
    Method entered.
    Thread starting sleep.
    Await 1 finished.
    Await 2 finished
    Thread stopped sleeping
    Method completed
    0
    
Mrinal Kamboj
  • 11,300
  • 5
  • 40
  • 74
0

AsyncContext schedules all tasks to be executed on a single thread. Your Method consists of Delays and WriteLines. You may think of Delays as analogous to IO operations in that they do not need a thread to be executed on. However, WriteLine requires a thread. Thus, when Method is awaken from Delay it waits for thread be available to execute WriteLine.

Actually the Method would block even if it does not contain WriteLines but only Delays because it needs a thread for the Delay to return to and to start new Delay, but that would be more difficult to notice without WriteLines.

dvorn
  • 3,107
  • 1
  • 13
  • 12