Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async tasks are evaluated twice

I use the following method to do some tasks asynchronously and at the same time:

public async Task<Dictionary<string, object>> Read(string[] queries)
{
    var results = queries.Select(query => new Tuple<string, Task<object>>(query, LoadDataAsync(query)));

    await Task.WhenAll(results.Select(x => x.Item2).ToArray());

    return results
        .ToDictionary(x => x.Item1, x => x.Item2.Result);
}

I want the method to call LoadDataAsync for each string in the array at the same time, then wait until all tasks are finished and return the result.

  • If I run the method like this, it invokes LoadDataAsync twice for each item, once at the await ... line, and once at the final .Result property getter.
  • If I remove the await ... line, Visual Studio warns me that the whole method would run in parallel, since there are not await calls inside of the method.

What am I doing wrong?

Are there better (shorter) ways to do the same?

like image 538
cheesus Avatar asked Sep 30 '14 15:09

cheesus


3 Answers

Once again:

If I could teach people one thing about LINQ it would be that the value of a query is an object that executes the query, not the results of executing the query.

You create the query once, producing an object which can execute the query. You then execute the query twice. You have unfortunately created a query that not only computes a value but produces a side effect, and therefore, executing the query twice produces the side effect twice. Do not make reusable query objects that produce side effects, ever. Queries are a mechanism for asking questions, hence their name. They are not intended to be a control flow mechanism, but that's what you're using them for.

Executing the query twice produces two different results because of course the results of the query could have changed between the two executions. If the query is querying a database, say, the database could have changed between executions. If your query is "what are all the last names of every customer in London?" the answer could change from millisecond to millisecond, but the question stays the same. Always remember, the query represents a question.

I would be inclined to write something without queries. Use "foreach" loops to create side effects.

public async Task<Dictionary<string, object>> Read(IEnumerable<string> queries)
{
    var tasks = new Dictionary<string, Task<object>>();
    foreach (string query in queries)
        tasks.Add(query, LoadDataAsync(query));
    await Task.WhenAll(tasks.Values);
    return tasks.ToDictionary(x => x.Key, x => x.Value.Result);
}
like image 159
Eric Lippert Avatar answered Nov 15 '22 19:11

Eric Lippert


You have to remember that LINQ operations return queries, not the results of those queries. The variable results doesn't represent the results of the operation that you have, but rather a query that is able to generate those results when iterated. You iterate it twice, executing the query on each of those occasions.

What you can do here is materialize the results of the query into a collection first, rather than storing the query itself, in results.

var results = queries.Select(query => Tuple.Create(query, LoadDataAsync(query)))
    .ToList();

await Task.WhenAll(results.Select(x => x.Item2));

return results
    .ToDictionary(x => x.Item1, x => x.Item2.Result);
like image 25
Servy Avatar answered Nov 15 '22 21:11

Servy


A better way might be to format the async calls in such a way that the results of the tasks are returned by the await with their respective keys:

public async Task<KeyValuePair<string, object>> LoadNamedResultAsync(string query)
{
    object result = null;
    // Async query setting result 
    return new KeyValuePair<string, object>(query, result)
}

public async Task<IDictionary<string, object>> Read(string[] queries)
{
    var tasks = queries.Select(LoadNamedResultAsync);
    var results = await Task.WhenAll(tasks);
    return results.ToDictionary(r => r.Key, r => r.Value);
}
like image 22
Jesse Sweetland Avatar answered Nov 15 '22 20:11

Jesse Sweetland