5

Is the creation of a Task a context switching point, or only when it starts to await or do other asynchronous things?

For example if I have these functions:

async Task Foo()
{
  Console.WriteLine("In Foo");
  await bar();
}

void CallFoo()
{
  var task = Foo();
  Console.WriteLine("Returned from Foo");
  task.Wait();
}

Is it possible for "Returned from Foo" to print before "In Foo"?

bfops
  • 5,348
  • 5
  • 36
  • 48
  • Do you have any issue with `Context`? or you are asking because of curiosity? – Sham Oct 05 '18 at 18:44
  • @PoulBak: Whether Foo is required to yield at the await is not the question that was asked. The question was whether it is permitted to yield *before* the first await. You've made a claim; can you back it up? Can you show a sample implementation of "Bar()" where "Returned from Foo" prints before "In Foo"? – Eric Lippert Oct 05 '18 at 20:58

2 Answers2

13

Is it possible for "Returned from Foo" to print before "In Foo"?

In the program as you've written it, no, that is not possible.

If you have a different program that runs a worker thread, then you have all the usual issues of ordering side effects between threads. There is nothing special about "async/await" that makes those issues go away.

Is the creation of a Task a context switching point, or only when it starts to await or do other asynchronous things?

Let's be precise by what we mean by a "context switching point".

A method in C# can do one of four things:

  • Run forever
  • Return normally
  • Throw
  • Suspend -- which is what I think you mean by "context switch".

A method marked async can suspend, but they only suspend when they await a non-completed awaitable. Awaiting a completed awaitable does not suspend, but it can throw.

Tasks returned by async methods are hot. That is, the asynchronous workflow begins and it runs until it returns, throws or suspends. It is possible to create a "cold" task with the Task constructor, and that doesn't start until you call Start on it. Normally you would not do that unless you were writing your own task scheduler.

Note that this is different than iterator blocks. A common mistake is:

IEnumerable<int> GetMeSomeInts(int x) 
{
  if (x < 0)
    throw new SomeException();
  for (int i = 0; i < x; i += i)
    yield return i;
}

If you say

var nums = GetMeSomeInts(-1); // 1
foreach(int num in nums)      // 2
   ...

Then the throw does not happen on line 1, it happens on line 2! Iterator blocks do not run up to the first yield when you call them. They suspend immediately and do not execute until iterated in the foreach. Be careful, particularly if you are working with both async and iterator coroutines in the same program.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
6

From the C# 5.0 language spec:

10.15.1 Evaluation of a task-returning async function

Invocation of a task-returning async function causes an instance of the returned task type to be generated. This is called the return task of the async function. The task is initially in an incomplete state.

The async function body is then evaluated until it is either suspended (by reaching an await expression) or terminates, at which point control is returned to the caller, along with the return task.

In the example in the question, Foo will immediately be evaluated until its first await before control is returned to the caller. "In Foo" will always be printed before "Returned from Foo".

Community
  • 1
  • 1
bfops
  • 5,348
  • 5
  • 36
  • 48
  • 1
    That's interesting language because it is possible for a Task to be in RanToCompletion state before being returned. – Crowcoder Oct 05 '18 at 20:06
  • 2
    @Crowcoder: It sure is. Suppose the asynchronous workflow represents *fetching a value from the internet and caching it*. The initial fetch could take a long time, so it should be asynchronous, but you should not have to pay the price of a yield the *second* time the method is called; it should return the cached value immediately. – Eric Lippert Oct 05 '18 at 20:52