第四章:CPU与线程分析
4.1 CPU使用率分析——别被数字骗了
CPU使用率,这个指标看起来简单,但坑特别多。我见过太多人盯着任务管理器里那个百分比,然后说「CPU才30%,系统很空闲啊」。嗯,真相往往没那么简单。
先说说CPU使用率的本质。它表示CPU处于忙碌状态的时间比例。但「忙碌」分两种:一种是真正在干活(用户态),另一种是在等资源(内核态)。我曾经排查过一个线上问题,CPU使用率只有15%,但请求延迟高得离谱。后来发现,大量线程在等待锁,CPU时间都花在了上下文切换上。
核心指标:
- User %:应用程序实际执行代码的时间
- System %:操作系统内核执行的时间(系统调用、中断等)
- I/O Wait %:CPU等待磁盘/网络I/O完成的时间
- Steal %:虚拟化环境下,被宿主机偷走的时间
我个人习惯用 perf 或 top 命令看这些细分指标。在Windows上,perfmon 里的「Processor Information」计数器也能看到类似数据。你想想看,如果System%很高,说明系统调用频繁,可能是锁竞争或者线程切换太猛了。
4.2 线程池调优——别让线程成为瓶颈
线程池是.NET后端最常用的并发工具。但调优不当,它反而会成为性能杀手。我记得有一次帮客户优化一个API服务,QPS上不去,线程池里线程数飙到200多,CPU却只有40%。
为什么会这样?因为线程池默认的最小线程数太保守了。在.NET Core中,默认最小线程数等于CPU核心数。对于I/O密集型任务,这个值远远不够。
调优建议:
- 对于I/O密集型应用(如数据库查询、HTTP调用),适当提高最小线程数
- 对于CPU密集型应用,保持线程数接近核心数
- 使用
ThreadPool.SetMinThreads和ThreadPool.SetMaxThreads调整
我曾经踩过一个坑:把最小线程数设得过高,结果大量线程在等待锁,反而导致性能下降。所以调优要结合具体场景,不能一刀切。
// 调整线程池示例
int workerThreads, completionPortThreads;
ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads);
// 对于I/O密集型,建议设置为 CPU核心数 * 2 或更高
ThreadPool.SetMinThreads(Environment.ProcessorCount * 4, completionPortThreads);
注意:不要频繁调用 ThreadPool.SetMinThreads。它应该在应用启动时设置一次。运行时动态调整可能导致线程池行为不稳定。
4.3 异步编程与性能——async/await的正确姿势
异步编程是.NET性能优化的核心武器。但用不好,反而会拖慢系统。我见过最典型的错误:在同步方法里调用异步方法,然后用 .Result 或 .Wait() 阻塞等待。这会导致线程池饥饿,甚至死锁。
异步编程的本质是什么?说白了,就是让线程在等待I/O时去做别的事。你想想看,一个线程如果卡在数据库查询上,它就不能处理其他请求。异步编程让这个线程「释放」出来,去处理其他请求。
4.3.1 async/await 最佳实践
- 全程异步:从控制器到数据访问层,全部使用 async/await
- 避免阻塞:不要用 .Result、.Wait()、Task.WaitAll()
- 配置上下文:在库代码中使用
ConfigureAwait(false) - 控制并发:使用 SemaphoreSlim 限制并发数
// 正确的异步模式
public async Task<User> GetUserAsync(int id)
{
// 使用 ConfigureAwait(false) 避免上下文捕获
var user = await _dbContext.Users.FindAsync(id).ConfigureAwait(false);
return user;
}
// 错误的做法——会导致死锁
public User GetUser(int id)
{
// 在同步方法中阻塞异步调用
return GetUserAsync(id).Result; // 危险!
}
性能对比:
| 场景 | 同步方式 | 异步方式 | 提升 |
|---|---|---|---|
| 100个并发请求 | 需要100个线程 | 只需几个线程 | 线程数减少90%+ |
| 内存占用 | 每个线程1MB栈空间 | 几乎无额外开销 | 内存节省显著 |
| 上下文切换 | 频繁切换 | 极少切换 | CPU效率提升 |
4.3.2 并发控制——别让异步变成灾难
异步编程虽然好,但并发控制不当也会出问题。我曾经遇到一个案例:某个服务同时发起1000个HTTP请求,结果目标服务器被打挂了。嗯,这就是典型的「异步风暴」。
解决方案很简单:使用 SemaphoreSlim 限制并发数。
private static readonly SemaphoreSlim _throttler = new SemaphoreSlim(50);
public async Task ProcessBatchAsync(List<int> ids)
{
var tasks = ids.Select(async id =>
{
await _throttler.WaitAsync();
try
{
await ProcessSingleAsync(id);
}
finally
{
_throttler.Release();
}
});
await Task.WhenAll(tasks);
}
小技巧:对于I/O密集型操作,并发数可以设置为目标服务能承受的最大值。对于CPU密集型操作,并发数建议不超过CPU核心数的2倍。
4.4 实战案例分析
最后分享一个我实际优化过的案例。一个电商API,高峰期CPU使用率80%,但响应时间超过5秒。通过分析发现:
- 线程池线程数飙到300+,上下文切换频繁
- 大量同步阻塞操作(.Result调用)
- 数据库查询没有使用异步方法
优化方案:
- 将所有数据库操作改为异步
- 调整线程池最小线程数为CPU核心数的4倍
- 使用
ConfigureAwait(false)减少上下文捕获 - 对第三方API调用使用SemaphoreSlim限流
优化后,CPU使用率降到35%,响应时间降到800ms。你看,有时候问题不在硬件,而在代码本身。
避坑指南:我曾经在优化时犯过一个错误——把所有方法都改成异步,包括那些只做CPU计算的方法。结果性能反而下降了。因为异步本身有开销,对于纯CPU计算,同步方式反而更快。记住:异步只对I/O密集型操作有效。
好了,这一章的内容就到这里。CPU和线程的优化,说白了就是让资源用在刀刃上。下一章我们会聊内存管理,那又是一个大坑。到时候见。