集合遍历:asParallel与异步函数的基准测试结果困惑解析
咱们一个个拆解这三个方法的行为和Benchmark结果的由来:
1. 同步遍历 ListLookup
这个方法用List.ForEach逐个处理元素,每个元素执行Thread.Sleep(2秒)——Thread.Sleep是阻塞当前线程的操作。因为是同步顺序执行,3个元素就是3 × 2秒 = 6秒,和你看到的6,027,116,440 ns(约6秒)完全吻合,这部分是符合预期的。
2. 并行遍历 ListLookupAsParallel
AsParallel()会把集合的处理任务分发到线程池的多个线程上并行执行。这里3个元素的Thread.Sleep(2秒)是同时进行的,所以总耗时只需要接近单个元素的处理时间(约2秒),对应结果里的2,008,655,313 ns(约2秒),这也是并行处理的正常表现——阻塞操作的并行化确实能缩短总耗时。
3. 异步遍历 ListLookupAsync(核心问题所在)
这个结果的异常完全是因为BenchmarkDotNet的测试逻辑和你的代码写法共同导致的:
- 首先,
IAsyncEnumerable是惰性执行的——调用ListLookupAsync()方法时,并不会立刻执行方法体里的循环和await Task.Delay,它只会返回一个异步枚举器对象,只有当你调用MoveNextAsync()(比如用await foreach遍历它)的时候,才会逐步执行里面的代码。 - 而BenchmarkDotNet默认情况下,对于返回
IAsyncEnumerable的方法,不会自动枚举它——你的基准测试实际上只测了「创建异步枚举器」这个极轻量的操作,完全没有执行到那些Task.Delay的逻辑,所以耗时才只有27ns左右,和实际的异步遍历耗时完全不沾边。
另外还要提一下你代码里的小问题:方法里第一个Task.Delay(2秒)没有加await,这个任务会被创建后立刻丢弃,根本不会等待,属于无用代码,不过这不是导致基准结果异常的主要原因——就算你去掉这行,只要Benchmark没枚举异步枚举器,结果还是会一样快。
如何正确测试异步遍历?
如果你想准确测试IAsyncEnumerable的遍历耗时,需要修改基准方法,让它实际枚举异步序列,比如改成这样:
[Benchmark] public async Task ListLookupAsync() { await foreach (var item in ListLookupAsyncEnumerable()) { // 遍历即可,不需要额外操作 } } // 把原来的异步枚举逻辑抽成单独方法 private async IAsyncEnumerable<int> ListLookupAsyncEnumerable() { foreach (var item in _list) { await Task.Delay(TimeSpan.FromSeconds(2)); yield return item; } }
这样修改后,Benchmark才会真正执行所有异步等待逻辑,结果会接近6秒(因为异步遍历是顺序执行每个await Task.Delay,和同步遍历的总耗时差不多,区别只是异步等待不会阻塞线程)。
内容的提问来源于stack exchange,提问作者Sameh Hamza

