为何异步等待Task内的同步代码比异步代码慢很多?
为什么异步HttpClient调用与同步.Result调用的性能差异如此显著?
这事儿的核心差异,本质上是**异步IO的“非阻塞等待”和同步调用的“阻塞线程等待”**之间的区别,再加上线程池的调度逻辑放大了这个差异,咱们一步步拆解:
1. 异步版本(6秒完成)的工作逻辑
你第一个版本里用await client.GetAsync()的时候,发生了这些事:
- 当发起网络请求后,当前线程会被立刻释放回线程池,不用傻等着网络响应回来。这个线程可以去处理其他等待执行的任务(比如另外59个请求里的某一个)。
- 当网络响应返回时,线程池会再分配一个线程(可能是之前的那个,也可能是新的)来继续处理后续的
ReadAsStringAsync()和结果收集。 - 60个请求几乎是同时发起的,大部分时间都是在等维基百科的服务器响应,而线程池只需要少量线程就能“周转”所有任务,不会出现线程资源不够用的情况,所以整体耗时接近网络请求的最长响应时间,自然很快。
2. 同步.Result版本(50秒完成)的问题所在
当你换成.Result同步调用时,情况完全变了:
- 每个
Task.Run里的线程池线程,在调用client.GetAsync().Result时,会被彻底阻塞,直到网络响应返回。这期间这个线程啥也干不了,就占着线程池的资源不动。 - 线程池的线程数量不是无限的,默认最小工作线程数是你的CPU核心数,超过这个数之后,线程池新增线程的速度是很慢的(大概每500毫秒才新增一个)。60个任务同时要线程,线程池根本跟不上,大量任务会排队等着分配线程。
- 就算线程池慢慢凑够了60个线程,每个线程都在阻塞等待网络,线程上下文切换的开销也会大幅增加,进一步拖慢整体速度。任务数量少的时候,线程池能快速分配足够线程,所以差异没那么明显。
额外的小提醒
你的代码里还有个隐藏问题:result.Add(data)不是线程安全的!多个线程同时往List<string>里加数据,会导致数据错乱或者抛出异常。建议换成ConcurrentBag<string>来收集结果。
内容的提问来源于stack exchange,提问作者FCin




