不使用Async方式调用.NET HttpClient是否属于不良开发实践?
阻塞式调用.Result在服务端是否属于不良实践
答案是肯定的:在ASP.NET Core的请求处理链路里,对还没执行完的Task调用.Result阻塞等待,确实是公认的不良实践。
虽然ASP.NET Core已经移除了旧版.NET Framework ASP.NET中的同步上下文,不会出现经典的异步死锁问题,但依然存在两个核心问题:
- 造成线程池线程浪费:发起外部HTTP请求这类IO操作时,本身不需要工作线程参与等待,调用
.Result会让当前处理请求的工作线程全程阻塞,什么工作都做不了,只能等IO返回,平白占用线程资源。 - 极端场景下触发线程池饥饿:当并发量上来后,阻塞的线程数超过线程池最小工作线程阈值时,线程池扩容速度极慢(每秒约新增1-2个线程),会直接导致请求排队、响应延迟飙升,甚至服务不可用。
另外你贴出的同步示例代码还存在两个独立的问题:
- 每次请求都new一个HttpClient实例,会造成套接字耗尽,生产环境极容易出故障,应该使用
IHttpClientFactory管理HttpClient生命周期。 - 调用
PostAsJsonAsync时传入手动构造的StringContent属于用法错误,PostAsJsonAsync会自动将传入的对象序列化为JSON内容,你这种写法会导致内容重复序列化,直接传payload对象即可,要发自定义StringContent应该用PostAsync方法。
为什么100次/分钟的负载测试看不到异步写法的性能提升
原因非常简单:你的测试负载太低了,根本没有触碰到阻塞写法的性能瓶颈。
100次/分钟换算下来每秒仅不到2个请求,就算每个外部HTTP请求耗时500ms,全程阻塞也只需要占用1个左右的工作线程,远低于线程池默认的最小工作线程数(通常和CPU核心数挂钩,8核机器默认最小就有8个工作线程),这种场景下两种写法的表现自然不会有可感知的差异。
异步写法的收益只有在高并发、或者IO等待时间较长的场景下才会体现:当并发请求数足够高,阻塞写法已经把线程池线程占满开始排队时,异步写法因为等待IO时会释放工作线程回线程池处理其他请求,能在相同硬件资源下支撑数倍甚至数十倍的吞吐量,且响应延迟更稳定。
异步写法本身几乎没有额外开销,ASP.NET Core框架本身从路由、中间件到控制器Action全链路都支持异步,不会因为你用了async/await就拖慢性能。
为什么微软不把.Result标记为过时方法
因为.Result本身有大量合法的使用场景,并不是只要用了就错:
- 当你明确知道Task已经处于完成状态时(比如用
Task.WhenAll/Task.WhenAny等待所有任务完成后,再取单个任务的Result),调用.Result不会有任何阻塞,也没有副作用,是完全合理的用法。 - 在非服务端场景,比如控制台程序的旧版Main方法(C#7.1之前不支持async Main)、或者单元测试中特定的阻塞场景,使用
.Result也不会有服务端那种线程池饥饿的问题。
一个API只有在绝大多数场景下都不应该使用、且有明确的替代方案时,才会被标记为过时。.Result显然不符合这个标准,它只是不适合在服务端请求链路中阻塞等待未完成的Task而已,属于用法场景选择问题,不是API本身的设计错误。
内容的提问来源于stack exchange,提问作者Cheung

