异步任务被评估两次
我使用以下方法异步并同时执行某些任务:
public async Task<Dictionary> Read(string[] queries) { var results = queries.Select(query => new Tuple<string, Task>(query, LoadDataAsync(query))); await Task.WhenAll(results.Select(x => x.Item2).ToArray()); return results .ToDictionary(x => x.Item1, x => x.Item2.Result); }
我希望该方法同时为数组中的每个字符串调用LoadDataAsync
,然后等待所有任务完成并返回结果。
- 如果我运行这样的方法,它会为每个项调用两次
LoadDataAsync
,一次在await ...
行,一次在最终的.Result
属性getter。 - 如果我删除
await ...
行,Visual Studio会警告我整个方法将并行运行,因为在方法内部没有await
调用。
我究竟做错了什么?
是否有更好(更短)的方法来做同样的事情?
再来一次:
如果我可以教人们关于LINQ的一件事,那就是查询的值是执行查询的对象,而不是执行查询的结果 。
您创建一次查询,生成一个可以执行查询的对象。 然后,您执行两次查询。 遗憾的是,您创建了一个不仅计算值而且产生副作用的查询,因此,执行两次查询会产生两次副作用。 不要制作产生副作用的可重用查询对象 。 查询是一种提问的机制,因此也就是他们的名字。 它们并非旨在成为一种控制流机制,而是您正在使用它们。
执行两次查询会产生两个不同的结果,因为查询的结果当然可以在两次执行之间发生变化 。 如果查询正在查询数据库,那么数据库可能在执行之间发生了变化。 如果您的查询是“伦敦每位客户的姓氏是什么?” 答案可以从毫秒变为毫秒,但问题保持不变。 永远记住,查询代表一个问题 。
我会倾向于写一些没有疑问的东西。 使用“foreach”循环创建副作用。
public async Task> Read(IEnumerable queries) { var tasks = new Dictionary>(); 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); }
您必须记住LINQ操作返回查询,而不是这些查询的结果。 变量results
不代表您拥有的操作的结果,而是表示在迭代时能够生成这些结果的查询 。 您迭代它两次,在每个场合执行查询。
您可以在此处执行的操作是首先将查询结果实现到集合中,而不是将查询本身存储在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);
更好的方法可能是格式化异步调用,以便await使用各自的键返回任务的结果:
public async Task> LoadNamedResultAsync(string query) { object result = null; // Async query setting result return new KeyValuePair(query, result) } public async Task> Read(string[] queries) { var tasks = queries.Select(LoadNamedResultAsync); var results = await Task.WhenAll(tasks); return results.ToDictionary(r => r.Key, r => r.Value); }
作为Jesse Sweetland答案的补充,完全实现的版本:
public async Task> LoadNamedResultAsync(string query) { Task
Rem:我建议将其作为编辑,但由于更改太多而被拒绝。