Asynchronous task with IO binding does not run asynchronously

I've spent a lot of time to understand the principles of asynchronous programming. But one thing remains 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 it, while the main thread is asleep, the IO bind operations from the method must be performed (make Task.Delay emulate IO) and interrupt the main thread sequentially to continue executing the 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 what Thread.Sleep

I am doing to stop the Main thread. But as I understand the () method doesn't need a thread because it consists of IO-bound operations. Can anyone please explain where I am not understanding it?

The AsynContext I'm using is ( here ).

+3


source to share


3 answers


Why is your code behaving correctly as expected?

When used, AsyncContext.Run

you provide an explicit context for the console application that it otherwise has NULL Synchronization Context

, now that you execute the following lines of code in MainAsync

:

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

      

Then Method()

execution starts where the statement is encountered:



await Task.Delay(1000);

It returns the control to the caller where you are blocking the context, creating a Thread Sleep for 10s Thread.Sleep(10000);

, so now until this sleep ends, Continuation cannot be executed in the Async method as it waits for the Continuation context to be available, at the time it is released, then it will start executing the continuation, but by then it will also have finished the rest of the instructions in MainAsync

, which will apparently be prioritized and the response will be as expected, it only expects in the end itself, actually checking the status of the Task for any logic. such as t.IsCompleted

, rather is a code smell, the best is await t

that waiting for the task to complete

There are two ways to get the expected behavior

  • As @Arik pointed out, set up both waits with ConfigureAwait(false)

    , what does it mean, just that it doesn't need the original context to continue continuing async and it will continue as a true Async operation, hence the result is as you expect. Most libraries with Async functionality, especially IO, implement ConfigureAwait(false)

    .
  • Make a call from Main like return MainAsync(args).Result;

    this will enforce the default behavior of Console apps, which means NULL Synchronization Context

    that Async doesn't care about continuing in any existing Context, it continues in the background even if you create a sleeping thread as it doesn't expect this context and result will be the same as you expect.

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

+1


source


By default, "wait" commits the current sync context and spawns a continuation in that original context. If the context is undefined, the continuation continues in the default thread pool (TaskScheduler.Default).

I'm not familiar with AsyncContext, but it probably spawns MainAsync in a synchronization synchronization context, and since Thread.Sleep is blocking the thread that occupies that context, the "wait" continuation will wait until the context is freed.

This is not a strange phenomenon, you can reproduce it without the AsyncContext class. Try running the same code in a Windows Forms app and you will see. Windows Forms have their own synchronization context, which protects against unsynchronized control manipulation.



To overcome this, you can say "wait" so as not to hijack the sync context 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");
        }

      

The wait will not try to resume a continuation in an existing context, rather it will spawn it in a thread pool task.

+3


source


AsyncContext schedules all tasks to run on a single thread. Your method consists of Delay and Record. You can think of latencies as analogous to I / O operations, since they don't need a thread to be executed. However, WriteLine requires a thread. Thus, when the method is awakened from Delay, it expects the thread to be available to execute WriteLine.

The method actually blocks even if it does not contain WriteLines, but only delays because it needs a thread to delay to return and start a new Delay, but this would be harder to notice without WriteLines.

0


source







All Articles