写 C# 后端或桌面程序时,常遇到接口一慢整页卡住、批量任务跑一整晚、CPU 只跑满一个核的情况。这篇不堆概念,只从开发者视角说明并发执行能带来哪些好处,配一套判断顺序和自查清单,帮你决定哪段代码值得改成并发。
写 C# 后端或桌面程序时,常遇到接口一慢整页卡住、批量任务跑一整晚、CPU 只跑满一个核。这类问题的根子往往不是代码写错了,而是该并发的地方还在串行执行。多线程并发不是银弹,但在特定场景下能同时改善响应速度和吞吐量。
这篇从开发者视角说清并发执行能带来哪些实际好处,配一套判断顺序和自查清单,帮你决定哪段代码值得改。全程不给绝对结论,只给判断标准。
好处一:界面与请求不再被一个慢操作拖死
同步阻塞调用会让 UI 线程或请求线程一直占着,用户看到的是转圈,后端的表现是连接被挂住。改成并发之后,主线程只负责调度和汇总,慢的那部分交给后台任务。最直观的体验提升在这里:页面可以先渲染骨架,数据到了再填。
典型场景是一个页面要拉三四份互相独立的数据。串行就是等一次网络往返再等下一次,总耗时约等于各次之和;并发之后总耗时接近最慢的那一次。注意前提是这几份数据之间没有依赖。
- 先画依赖图:哪些调用之间没有先后关系,只有这些才能并发
- 主线程只做调度,不做实际等待,UI 才不会被冻住
- 给每个并发任务单独设超时,不要让一个慢任务拖住整批结果
- 部分失败要能单独处理,不能因为一份数据失败就丢掉其他已经拿到的结果
好处二:批量任务的墙钟时间明显缩短
批量任务是最容易看到并发收益的地方。要先把任务分类:IO 密集型包括调接口、读写文件、查数据库,这类任务线程大部分时间在等,用异步并发就能让等待重叠起来;CPU 密集型是计算、序列化、加解密,这类靠并行吃满多核。
分类搞错会白忙。IO 密集堆一堆线程,线程还是空等,只是把线程池占满;CPU 密集用异步,计算还是排在一个核上跑。判断办法很简单:看任务在等的时候,CPU 占用是不是很低。低就是 IO 密集。
| 任务特征 | 优先手段 | 不适用的情况 |
|---|---|---|
| 等待外部响应、查库、读写文件 | 异步并发 | 下游有并发上限或限流时不能无限放开 |
| 纯计算、编码解码、图像处理 | 并行执行 | 机器本身核数很少或已被其他进程占满 |
| 混合型,边算边等 | 分段处理,计算段并行、等待段异步 | 状态共享多、锁竞争重时收益会被吃掉 |
| 任务之间有强顺序依赖 | 串行或流水线 | 任何形式的并发都不划算 |
好处三:CPU 与连接资源用得更满
单线程程序在多核机器上通常只占一个核,剩下的算力白放着。把可以并行的计算拆开之后,核数越多收益越明显,这是并行最直接的动机。不过收益不是线性的,任务切分和结果合并本身也要花时间。
线程池的价值在于复用线程,省掉频繁创建销毁的开销。但池子大小要设,不是越大越好。池子过大时线程之间抢 CPU、抢锁、抢内存带宽,上下文切换变多,整体反而变慢。判断依据是看 CPU 占用和任务排队长度的关系:加线程之后占用没上去、排队还长,说明瓶颈不在线程数。
- 线程池
- 复用已创建的线程来跑任务,避免每次新建的开销,但容量要显式设定
- 上下文切换
- CPU 在多个线程之间换着跑也要花时间,线程过多时这部分开销会吃掉并发收益
- 下游容量
- 数据库连接数、第三方接口并发上限这些外部约束,往往才是真正的天花板
并发的代价:这几类情况别盲目上
并发的收益是有代价的,代价主要体现在三块:共享状态的协调、问题排查的复杂度、外部系统的承受能力。共享状态要么加锁,要么改成不可变数据,锁竞争严重的时候并发版可能比串行还慢。
排查复杂度是另一种隐性成本。串行程序的出错调用栈是一条线,并发程序出错时栈是多条交错的,日志时间戳也未必能直接说明先后。如果团队没有对应的日志和追踪手段,改并发之前先补上这块。
- 共享状态多、写操作频繁的代码,先考虑拆分数据或改不可变,再谈并发
- 涉及钱、库存这类必须严格顺序的操作,不要为了快而放松一致性
- 外部接口有并发上限时,先确认对方能承受多少,再定自己的并发数
- 日志里带上任务标识,否则并发出问题时根本分不清哪条对应哪个任务
落地自查清单:改并发前先过一遍
改并发最容易犯的错是凭感觉判断「变快了」。下面这套顺序按可执行性排,建议逐条走完再动手。
- 量基线串行跑一遍,记录耗时和资源占用,改完之后用同样的输入和同样的机器再跑一遍对比,别用不同条件的结果互相对照
- 分类任务IO 密集用异步,CPU 密集用并行,混着写容易两头不讨好;判断依据是等待期间 CPU 占用高不高
- 设上限并发数、超时时间、失败重试次数都要显式配置,不能无限放开,上限参考下游系统的承受能力
- 留退路并发失败时要能退回串行或降级处理,别让一次改动静默地把整个流程拖垮
- 看数字改完之后到站内的速度测试和价格追踪页面核对当时的实测数字,别拿印象当结论
默认并发数、超时这些参数没有通用推荐值,它们跟你的机器核数、下游容量、任务大小都有关。想知道同类服务在中转场景下的实际表现差异,可以去站内看当时的对比数据,而不是照搬别人文章里的固定值。
下一步如果你正在选 API 中转服务,并发能力和稳定性往往比单价更影响实际体验,建议到站内工具目录实测一下各家在你所在网络环境下的并发表现,再决定用哪家。→常见问题
多线程一定比单线程快吗?
不一定。如果任务本身串行依赖强,或者瓶颈在下游接口和数据库,加线程只会增加切换开销和报错概率。判断方法很土:先串行跑一遍记录耗时,再按 IO 和 CPU 分类,只对真正互相独立、且瓶颈在本地等待的任务做并发。
async/await 和多线程是一回事吗?
不是。async/await 主要解决的是等待期间不占线程,底层靠状态机调度;多线程是真的开多个执行单元去跑。IO 密集任务用异步更省资源,CPU 密集任务靠并行才能吃满多核。两者可以配合用,但要清楚各自解决的是哪一类等待。
并发数设多少合适?
没有通用数字。先看两端上限:本机核数决定 CPU 密集任务的天花板,下游接口的限流额度决定 IO 密集任务的天花板。取两者较小值,再留一点余量。上线后用日志观察错误率和耗时曲线,慢慢调,别一次设很大。具体限流口径可以到站内对照页面核对。