将任务字典转换为结果字典

9
我有一个程序需要处理多个对象并生成分析结果。每个分析结果都是一个字符串,这些字符串被连接起来创建报告。报告需要按照特定顺序呈现结果,但我想要异步分析每个条目,因此我将所有内容放入字典中,然后可以在准备最终输出之前对其进行排序。
注意:为了本例子的简单起见,我假装我们正在分析当前程序集中的类型,尽管在我的情况下比这更复杂。
我认为实现这个的基本模式如下:
var types = myAssembly.GetTypes();
var tasks = types.ToDictionary( key => key, value => AnalyzeType(value) );
//AnalyzeType() is an async method that returns Task<string>.

现在我们有一个热门任务的字典,可能在创建字典时已经完成,也可能没有完成,因为我没有等待任何事情。
现在要获取结果。我该怎么做?
等待
理论上,我只需要等待每个任务;等待操作的结果是值本身。但这并不会转换任何东西。
var results = tasks.ToDictionary( k => k.key, async v => await v.Value );
Console.WriteLine(results.GetType().FullName);

输出:

System.Collections.Generic.Dictionary'2[[System.Type, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Threading.Tasks.Task'1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

我感到困惑…… 我以为在Task前面加上await,C#会将其转换为结果。但我仍然得到了一个任务字典。

GetResult()

另一种方法是使用这个:

var results = tasks.ToDictionary( key => key, value => value.GetAwaiter().GetResult() );
Console.WriteLine(results.GetType().FullName);

输出:

System.Collections.Generic.Dictionary'2[[System.Type, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

因此,这似乎可以得到我想要的结果,但是我必须删除await关键字,现在编译器会发出警告,表示该方法将同步执行。

我可以添加

await Task.WhenAll(tasks.Select( kvp => kvp.Value));

我希望在所有任务完成运行之前不要放弃控制权(因为这可能需要一段时间),所以我的整体解决方案是:

await Task.WhenAll(tasks.Select( kvp => kvp.Value));
var results = tasks.ToDictionary( key => key, value => value.GetAwaiter().GetResult() );
Console.WriteLine(results.GetType().FullName);

我猜这个方法能够运行。但是似乎不是正确的方法;我对调用GetAwaiter().GetResult()持怀疑态度,并且如果不需要的话,我宁愿不进行额外的WhenAll()步骤,而实际上也不应该进行,因为我正在单独获取每个任务的等待者和结果。

什么是正确的方法?为什么我的第一个示例中await关键字没有起作用?我需要使用GetResult()吗?如果需要,是否包括await Task.WhenAll()是一个好主意,还是更好地仅依赖于稍后发生的GetAwaiter()调用?

点击此处查看 Fiddle,如果您想使用它。

编辑(答案):

感谢 Shaun 提供正确答案。如果有人想要将其放入他们的代码库中,这里是一个通用的扩展方法。

public static async Task<Dictionary<TKey, TResult>> ToResults<TKey,TResult>(this IEnumerable<KeyValuePair<TKey, Task<TResult>>> input)
{
    var pairs = await Task.WhenAll
    (
        input.Select
        ( 
            async pair => new { Key = pair.Key, Value = await pair.Value }
        )
    );
    return pairs.ToDictionary(pair => pair.Key, pair => pair.Value);
}

为什么在 WhenAll 之后会出现这样奇怪的 value => value.GetAwaiter().GetResult()?普通的 value.Result 就可以了... - Alexei Levenkov
@AlexeiLevenkov,因为value.GetAwaiter().GetResult()的方式更好,它不会像await那样抛出AggregateException, 而是抛出第一个异常。它更接近await的行为,但仍然是阻塞的,因此并不是真正需要的。 - the berserker
3个回答

11

为什么在我的第一个示例中,await 关键字没有起作用?

await 关键字在 async 方法的上下文中解开 Task<T>,操作类型为 <T> 的底层结果,并将 async 方法的返回值重新包装在 Task 中。这就是每个 async 方法/函数返回 voidTaskTask<T> 之一 的原因(请注意,void 只适用于事件)。async 方法不返回未解开的值;我们永远不会看到像 public async int SomeMethod() 这样的方法签名,因为在 async 方法中返回 int 不会编译通过,而应该返回一个 Task<int>

正确的做法是什么?

以下是一种将值类型为 Task<T> 的字典转换为值类型为 <T> 的字典的方法(使用 Fiddle 进行演示):

using System.Threading.Tasks; 
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public async static void Main()
    {
        // create a dictionary of 10 tasks
        var tasks = Enumerable.Range(0, 10)
            .ToDictionary(i => i, i => Task.FromResult(i * i));

        // await all their results
        // mapping to a collection of KeyValuePairs
        var pairs = await Task.WhenAll(
            tasks.Select(
                async pair => 
                    new KeyValuePair<int, int>(pair.Key, await pair.Value)));

        var dictionary = pairs.ToDictionary(p => p.Key);

        System.Console.WriteLine(dictionary[2].Value); // 4
    }
}

谢谢您的回答。这确实有助于澄清事情。然而,您的答案生成了一个无法按键排序的 IEnumerable<TResult>,而这正是我们的目标。需要使用 IEnumerable<KeyValuePair<TKey,TResult>>Dictionary<TKey,TResult> - John Wu
@JohnWu 很好的建议。我更新了代码,现在创建了一个结果字典。 - Shaun Luttin

1
解决方案是摆脱 ToDictionary 调用并自己构建字典:
var result = new Dictionary<Type, string>();
foreach (var kvp in tasks) 
{ 
    result[kvp.Key] = await kvp.Value;
}

ToDictionary无法正常工作的原因是您希望从任务中提取值而不是另一个任务。创建异步lambda只会创建另一个任务,然后您必须等待它。

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接