异步任务将评估两次

本文关键字:两次 任务 评估 异步 | 更新日期: 2023-09-27 18:25:03

我使用以下方法异步地同时执行一些任务:

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);
}

我希望该方法同时为数组中的每个字符串调用LoadDataAsync,然后等待所有任务完成并返回结果。

  • 如果我运行这样的方法,它会为每个项调用LoadDataAsync两次,一次在await ...行,一次是在最后的.Result属性getter
  • 如果删除await ...行,Visual Studio会警告我整个方法将并行运行,因为该方法内部没有await调用

我做错了什么

有更好(更短)的方法吗

异步任务将评估两次

再一次:

如果我能教人们一件关于LINQ的事情,那就是查询的值是执行查询的对象,而不是执行查询的结果

您创建一次查询,生成一个可以执行查询的对象。然后执行查询两次。不幸的是,您创建的查询不仅计算值,而且会产生副作用,因此,执行两次查询会产生两次副作用永远不要生成会产生副作用的可重用查询对象。查询是提出问题的一种机制,因此得名。它们并不是一种控制流机制,但这正是你使用它们的目的。

执行两次查询会产生两个不同的结果,因为查询的结果当然会在两次执行之间发生变化。如果查询是在查询数据库,那么数据库可能在执行之间发生了更改。如果你的问题是"伦敦每个客户的姓氏都是什么?"答案可能会从一毫秒变为一毫秒,但问题保持不变。永远记住,查询表示一个问题

我倾向于写一些没有疑问的东西。使用"foreach"循环来创建副作用。

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);
}

您必须记住,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);

一个更好的方法可能是格式化异步调用,使任务的结果由等待返回,并带有各自的键:

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);
}

作为Jesse Sweetland答案的补充,完全实现的版本:

public async Task<KeyValuePair<string, object>> LoadNamedResultAsync(string query)
{
    Task<object> getLoadDataTask = await LoadDataAsync(query);
    return new KeyValuePair<string, object>(query, getLoadDataTask.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);
}

雷姆:我提议将其作为编辑,但由于修改太多而被拒绝