6

Given a simple Hotel entity as an example:

class Hotel
{
    public int NumberOfRooms { get; set; }
    public int StarRating { get; set; }
}

Please consider the following code in C# 5.0:

public void Run()
{
    var hotel = new Hotel();
    var tasks = new List<Task> { SetRooms(hotel), SetStars(hotel) };
    Task.WaitAll(tasks.ToArray());
    Debug.Assert(hotel.NumberOfRooms.Equals(200));
    Debug.Assert(hotel.StarRating.Equals(5));
}

public async Task SetRooms(Hotel hotel)
{
    await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
    hotel.NumberOfRooms = 200;
}

public async Task SetStars(Hotel hotel)
{
    await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
    hotel.StarRating = 5;
}

Both calls to Debug.Assert() pass successfully. I don't understand how after both tasks have completed, the instance of Hotel contains the assignment from both the methods that run in parallel.

I thought that when await is called (in both SetRooms() and SetStars()), a "snapshot" of the hotel instance is created (having both NumberOfRooms and StarRating set to 0). So my expectation was that there will be a race condition between the two tasks and the last one to run will be the one copied back to hotel yielding a 0 in one of the two properties.

Obviously I am wrong. Can you explain where I'm misunderstanding how await works?

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
urig
  • 16,016
  • 26
  • 115
  • 184

1 Answers1

16

I thought that when await is called (in both SetRooms() and SetStars()), a "snapshot" of the hotel instance is created

Your Hotel class is a reference type. When you use async-await, your method is transformed into a state-machine, and that state-machine hoists the reference to your variable onto it. This means that both state-machines created are pointing at the same Hotel instance. There is no "snapshot" or deep copy of your Hotel, the compiler doesn't do that.

If you want to see what actually goes on, you can have a look at what the compiler emits once it transforms your async methods:

[AsyncStateMachine(typeof(C.<SetRooms>d__1))]
public Task SetRooms(Hotel hotel)
{
    C.<SetRooms>d__1 <SetRooms>d__;
    <SetRooms>d__.hotel = hotel;
    <SetRooms>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
    <SetRooms>d__.<>1__state = -1;
    AsyncTaskMethodBuilder <>t__builder = <SetRooms>d__.<>t__builder;
    <>t__builder.Start<C.<SetRooms>d__1>(ref <SetRooms>d__);
    return <SetRooms>d__.<>t__builder.Task;
}
[AsyncStateMachine(typeof(C.<SetStars>d__2))]
public Task SetStars(Hotel hotel)
{
    C.<SetStars>d__2 <SetStars>d__;
    <SetStars>d__.hotel = hotel;
    <SetStars>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
    <SetStars>d__.<>1__state = -1;
    AsyncTaskMethodBuilder <>t__builder = <SetStars>d__.<>t__builder;
    <>t__builder.Start<C.<SetStars>d__2>(ref <SetStars>d__);
    return <SetStars>d__.<>t__builder.Task;
}

You can see that both methods hoist the hotel variable into their state-machine.

So my expectation was that there will be a race condition between the two tasks and the last one to run will be the one copied back to hotel yielding a 0 in one of the two properties.

Now that you see what the compiler actually does, you can understand that there really isn't a race condition. It's the same instance of Hotel which is being modified, each method setting the different variable.


Side note

Perhaps you wrote this code just as an example to explain your question, but if you're already creating async methods, I'd recommend using Task.WhenAll instead of the blocking Task.WaitAll. This means changing the signature of Run to async Task instead of void:

public async Task RunAsync()
{
    var hotel = new Hotel();
    await Task.WhenAll(SetRooms(hotel), SetStars(hotel));
    Debug.Assert(hotel.NumberOfRooms.Equals(200));
    Debug.Assert(hotel.StarRating.Equals(5));
}
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • Thanks @yuval-itzchakov. What is the advantage of using `Task.WhenAll()` over `Task.WaitAll()`? – urig Dec 14 '15 at 10:44
  • 1
    `Task.WhenAll` is non-blocking, thus asynchronously yielding control to the calling method, as opposed to `Task.WaitAll` which synchronously blocks. Also, exception handling is a little different. See [this](http://stackoverflow.com/questions/25009437/running-multiple-async-tasks-and-waiting-for-them-all-to-complete/25010220#25010220) question for more. – Yuval Itzchakov Dec 14 '15 at 10:46
  • Seeing as I will immediately call `Wait()` on the the Task` returned from `Task.WhenAll()`, does the advantage still hold? – urig Dec 14 '15 at 10:51
  • @urig No, it doesn't :) Any reason you must immediately block on it? – Yuval Itzchakov Dec 14 '15 at 10:55
  • @urig, you should never call `Wait()` on an async task, you're almost guaranteed to get a deadlock if you do it from the UI thread. In this case it works, but only because both methods use `ConfigureAwait(false)` internally. – Thomas Levesque Dec 14 '15 at 10:57
  • @ThomasLevesque You're right, but *almost guaranteed* really depends on the existence of a custom `SynchronizationContext` or not. If he's running this from a unit-test or a console application, that won't be an issue. But you're absolutely right about the fact that blocking on async code is usually a bad idea. – Yuval Itzchakov Dec 14 '15 at 10:58
  • 1
    @YuvalItzchakov, that's why I said "if you do it from the UI thread" ;) – Thomas Levesque Dec 14 '15 at 11:08
  • @YuvalItzchakov Only reason is I need to end the chain of awaits somewhere, no? :) – urig Dec 14 '15 at 11:18
  • FYI, this is running in ASP.net. Also, `ConfigureAwait(false)` is intentional. – urig Dec 14 '15 at 11:19
  • 2
    @urig You don't need to block with `Wait`. Make your action return a `Task` and you can await in the controllers action as well – Yuval Itzchakov Dec 14 '15 at 11:52