How does C # Task.WaitAll () merge the state of an object into one?

Given a simple hotel facility 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 () completed successfully. I do not understand how, after completing both tasks, the Hotel instance contains an assignment from both methods that work in parallel.

I thought that when calling await (both in SetRooms() and SetStars() ) a "snapshot" of the hotel instance (having both NumberOfRooms and StarRating ) is set to 0. Thus, I expected that between these two the tasks will be the race condition, and the last one will be copied back to the hotel , giving 0 in one of two properties.

Obviously, I'm wrong. Can you explain where I don’t understand how waiting works?

+6
source share
1 answer

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

Your Hotel class is a reference type. When you use async-wait, your method is converted to a state-machine, and this state machine raises a link to your variable for it. This means that both machine states created point to the same Hotel instance. There is no β€œsnapshot” or deep copy of your Hotel ; the compiler does not.

If you want to see what actually happens, you can see what the compiler emits as soon as it converts 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 raise the Hotel variable to their state.

So, I expected that between the two tasks and the last one to run would be copied back to the hotel giving 0 in one of two properties.

Now that you see what the compiler actually does, you can understand that this is not really a race condition. This is the same instance of Hotel that is changing, with each method setting a different variable.


Side note

You may have written this code as an example to explain your question, but if you are already creating async methods, I would recommend using Task.WhenAll instead of locking Task.WaitAll . This means changing the Run signature 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)); } 
+16
source

All Articles