Question

I am new to async/await and am tinkering with it to execute an operation on a list of objects using a list of tasks. I used Linq to generate both the list of objects and the list of tasks. The example below looks a little contrived, but it is a simplified version of my actual code.

I found that when the code is executed as shown, after all tasks have completed (after the await), none of the object's Now properties have been updated, and all of the tasks still have a status of Running.

I found that by eliminating Linq deferred execution by converting both objects and tasks to actual lists via .ToList<>(), my code worked as expected (objects populated, tasks all run to completion).

I am familiar with Linq deferred execution but I'm really confused by what is (isn't) going on in this code. I'm probably making a noob mistake with async/await...what is it?

private class Foo {
    public DateTime Now { get; set; }
}

private void Button_Click( object sender, EventArgs e ) {
    PopulateDates();
}

private async void PopulateDates() {
    var ordinals = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, };

    var foos = ordinals.Select( o => new Foo() ); //.ToList();

    var tasks = foos.Select( f => PopulateDateAsync( f ) ); //.ToList();

    await Task.WhenAll( tasks );

    var firstNow = foos.ElementAt( 0 ).Now;
    var firstTaskStatus = tasks.ElementAt( 0 ).Status;
}

private Task PopulateDateAsync( Foo foo ) {
    return Task.Run( () => PopulateDate( foo ) );
}

private void PopulateDate( Foo foo ) {
    Thread.Sleep( 2000 );
    foo.Now = DateTime.Now;
}
Was it helpful?

Solution

Your problem is due to LINQ's deferred execution. In particular, Task.WhenAll is appropriately waiting for the tasks to complete. However, when you call ElementAt, the sequence is re-evaluated, creating a new Foo and Task.

So, this would also not work:

var ordinals = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, };

var foos = ordinals.Select( o => new Foo() ); //.ToList();

// Get the first Foo, creating it.
var first = foos.ElementAt(0);

// This gets a *different* Foo. It creates it again.
var other = foos.ElementAt(0);

MessageBox.Show((first == other).ToString()); // Displays "false"

In general, it's a good idea to "reify" your sequence (using ToArray or similar) when dealing with any operations with side effects, including starting async operations. Task.WhenAll will reify your sequence internally, but then if you evaluate it again (e.g., ElementAt), you get unexpected behavior.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top