Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is Enumerator.MoveNext not working as I expect it when used with using and async-await?

Tags:

I would like to enumerate through a List<int> and call a async method.

If I do this in this way:

public async Task NotWorking() {   var list = new List<int> {1, 2, 3};    using (var enumerator = list.GetEnumerator()) {     Trace.WriteLine(enumerator.MoveNext());     Trace.WriteLine(enumerator.Current);      await Task.Delay(100);   } } 

the result is:

True 0 

but I expect it to be:

True 1 

If i remove the using or the await Task.Delay(100):

public void Working1() {   var list = new List<int> {1, 2, 3};    using (var enumerator = list.GetEnumerator()) {     Trace.WriteLine(enumerator.MoveNext());     Trace.WriteLine(enumerator.Current);   } }  public async Task Working2() {   var list = new List<int> {1, 2, 3};    var enumerator = list.GetEnumerator();   Trace.WriteLine(enumerator.MoveNext());   Trace.WriteLine(enumerator.Current);    await Task.Delay(100); } 

the output is as expected:

True 1 

Can anyone explain that behavior to me?

like image 803
Peter Punzenberger Avatar asked Mar 24 '15 10:03

Peter Punzenberger


People also ask

How does await foreach work?

This method is used when you have a bunch of tasks that you want to await all at once. We then await the method and wait for all the Tasks in our collection to complete. Once done, the method returns its Task as completed to its caller and our logic is complete. This solves our original issue in the first code snippet.

Is yield return Async?

Using an async yield return statement requires that the method be asynchronous, making use of async/await. Usually an async method will return a task. Your first thought when using yield return in your async method may be to have the method return Task of IEnumerable.

What is IAsyncEnumerable?

Introduction to IAsyncEnumerable<T> Async Streams or IAsyncEnumerable<T> provides a way to iterate over an IEnumerable collection asynchronously while using the yield operator to return data as it comes in.

What is async await in C#?

An async keyword is a method that performs asynchronous tasks such as fetching data from a database, reading a file, etc, they can be marked as “async”. Whereas await keyword making “await” to a statement means suspending the execution of the async method it is residing in until the asynchronous task completes.


2 Answers

Here's the short of this problem. A longer explanation follows.

  • List<T>.GetEnumerator() returns a struct, a value type.
  • This struct is mutable (always a recipe for disaster)
  • When the using () {} is present, the struct is stored in a field on the underlying generated class to handle the await part.
  • When calling .MoveNext() through this field, a copy of the field value is loaded from the underlying object, thus it is as though MoveNext was never called when the code reads .Current

As Marc mentioned in the comments, now that you know of the problem, a simple "fix" is to rewrite the code to explicitly box the struct, this will make sure the mutable struct is the same one used everywhere in this code, instead of fresh copies being mutated all over the place.

using (IEnumerator<int> enumerator = list.GetEnumerator()) { 

So, what happens really here.

The async / await nature of a method does a few things to a method. Specifically, the entire method is lifted onto a new generated class and turned into a state machine.

Everywhere you see await, the method is sort of "split" so that the method has to be executed sort of like this:

  1. Call initial part, up until the first await
  2. The next part will have to be handled by a MoveNext sort of like an IEnumerator
  3. The next part, if any, and all subsequent parts, are all handled by this MoveNext part

This MoveNext method is generated on this class, and the code from the original method is placed inside it, piecemeal to fit the various sequencepoints in the method.

As such, any local variables of the method has to survive from one call to this MoveNext method to the next, and they are "lifted" onto this class as private fields.

The class in the example can then very simplistically be rewritten to something like this:

public class <NotWorking>d__1 {     private int <>1__state;     // .. more things     private List<int>.Enumerator enumerator;      public void MoveNext()     {         switch (<>1__state)         {             case 0:                 var list = new List<int> {1, 2, 3};                 enumerator = list.GetEnumerator();                 <>1__state = 1;                 break;              case 1:                 var dummy1 = enumerator;                 Trace.WriteLine(dummy1.MoveNext());                 var dummy2 = enumerator;                 Trace.WriteLine(dummy2.Current);                 <>1__state = 2;                 break; 

This code is nowhere near the correct code, but close enough for this purpose.

The problem here is that second case. For some reason the code generated reads this field as a copy, and not as a reference to the field. As such, the call to .MoveNext() is done on this copy. The original field value is left as-is, so when .Current is read, the original default value is returned, which in this case is 0.


So let's look at the generated IL of this method. I executed the original method (only changing Trace to Debug) in LINQPad since it has the ability to dump the IL generated.

I won't post the whole IL code here, but let's find the usage of the enumerator:

Here's var enumerator = list.GetEnumerator():

IL_005E:  ldfld       UserQuery+<NotWorking>d__1.<list>5__2 IL_0063:  callvirt    System.Collections.Generic.List<System.Int32>.GetEnumerator IL_0068:  stfld       UserQuery+<NotWorking>d__1.<enumerator>5__3 

And here's the call to MoveNext:

IL_007F:  ldarg.0      IL_0080:  ldfld       UserQuery+<NotWorking>d__1.<enumerator>5__3 IL_0085:  stloc.3     // CS$0$0001 IL_0086:  ldloca.s    03 // CS$0$0001 IL_0088:  call        System.Collections.Generic.List<System.Int32>+Enumerator.MoveNext IL_008D:  box         System.Boolean IL_0092:  call        System.Diagnostics.Debug.WriteLine 

ldfld here reads the field value and pushes the value on the stack. Then this copy is stored in a local variable of the .MoveNext() method, and this local variable is then mutated through a call to .MoveNext().

Since the end result, now in this local variable, is newer stored back into the field, the field is left as-is.


Here is a different example which makes the problem "clearer" in the sense that the enumerator being a struct is sort of hidden from us:

async void Main() {     await NotWorking(); }  public async Task NotWorking() {     using (var evil = new EvilStruct())     {         await Task.Delay(100);         evil.Mutate();         Debug.WriteLine(evil.Value);     } }  public struct EvilStruct : IDisposable {     public int Value;     public void Mutate()     {         Value++;     }      public void Dispose()     {     } } 

This too will output 0.

like image 180
Lasse V. Karlsen Avatar answered Oct 20 '22 09:10

Lasse V. Karlsen


Looks like a bug in the old compiler, possibly caused by some interference of code transformations performed in using and async.

Compiler shipping with VS2015 seems to get this correctly.

like image 32
VSadov Avatar answered Oct 20 '22 09:10

VSadov