How to disallow Task from being interrupted by await?

xox*_*xox 1 .net c# task-parallel-library async-await

Coming from JavaScript I am used to the fact that code execution does not interleave, with await expressions being an exception, which define fixed re-entry points.
The .NET asynchrony semantics appear to be different though: During every call of Thread.Sleep, a Task waiting to be run jumps in and performs work. Then, on return of of the Sleep call, the Task's computation is interrupted immediately, as the code below demonstrates.

How can I achieve atomic execution of the loop? (Without locks or the like; assuming a single thread)

static async Task RunExperiment()
{
    var task = DoNotInterruptMe();
    Console.WriteLine("sleep...");
    Thread.Sleep(60);
    Console.WriteLine("sleep...");
    Thread.Sleep(60);
    Console.WriteLine($"Interrupted, because i={i}");
    Console.WriteLine("sleep...");
    Thread.Sleep(100);
    Console.WriteLine($"Interrupted again, because i={i}");
    await task;
    Console.WriteLine("task.Status: "+task.Status);
}

private static int i;

static async Task DoNotInterruptMe()
{
    Console.WriteLine("I: delay...");
    await Task.Delay(100);
    Console.WriteLine("I: working...");
    int j = 1;
    for (i = 0; i < 1<<30; i++)
    {
        j *= i;
    }
    Console.WriteLine("I: finished work");
}
Run Code Online (Sandbox Code Playgroud)

Run above using

RunExperiment().Wait();
Run Code Online (Sandbox Code Playgroud)

I am using Thread.Sleep here, but await Task.Delay(60) behaves just alike.

Sample output:

I: delay...
sleep...
sleep...
I: working...
Interrupted, because i=358449
sleep...
Interrupted again, because i=8598631
I: finished work
task.Status: RanToCompletion
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 7

Based on your usage of Console.WriteLine, I assume this is a console app. Await behaves differently in console apps than it does in windows forms apps.

Let's take a step back and say what await does:

  • Check to see if the awaitable (a task, in this case) is complete.
  • If it is completed abnormally, throw that exception, and have the current thread handle it.
  • If it is completed normally, fetch its value, if any, and keep going on the current thread.
  • If it is not complete, schedule the remainder of the method to run as the continuation of the task, bound to the current thread context when the task is complete. Then return to your caller.

It's that last bit that you are confused about. In a console application the continuation of the task may be scheduled onto a worker thread by the default console context.

In a windows forms app, the default context is to schedule the continuation on the current apartment, which we can do because we post a message to that apartment's message loop that causes the continuation to run. But there is no message loop in a console app.

Let's demonstrate this by rewriting your program:

static void Write(string s)
{
    Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:{s}");
}
Run Code Online (Sandbox Code Playgroud)

And now we change every other Console.WriteLine to call Write, and run your program and we see:

1:I: delay...
1:sleep...
1:sleep...
6:I: working...
1:Interrupted, because i=4061320
1:sleep...
1:Interrupted again, because i=26763596
6:I: finished work
6:task.Status: RanToCompletion
Run Code Online (Sandbox Code Playgroud)

Thread 1 starts the delay, and we then schedule the remainder of that workflow to run when the delay is complete. We immediately return and sleep. Meanwhile, the delay finishes and signals the task to run the remainder of its workflow, which it does on thread 6.

Back on thread 1, the sleeps finish and we discover that i has been updated -- I note that you forgot to make i volatile, but luckily we got the update.

Thread 1 then awaits the task associated with the asynchronous workflow, which is still running on thread 6, so it returns to its caller, and signs up the remainder of its workflow as the continuation of that task.

That task then completes on thread 6, and the continuation is now ready to be scheduled onto a worker thread. But thread 6 is now idle, so it gets scheduled and runs the remainder of the task.

This is to be expected in a console application. If you need fine-grained control over what thread a continuation is scheduled to run on, then you need to learn about how thread contexts work. Or, write an application that has a message pump, and the default context will keep everything on the same thread.