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?
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));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With