在Windows平台上做资源监控,C#开发者最常碰到的需求之一就是读取当前CPU占用率。系统自带的性能计数器体系记录了大量运行时指标,而.NET提供的PerformanceCounter类正是访问这些指标的官方通道。理解它的工作机制,比直接复制网上的代码片段更重要,因为错误的调用顺序会让你的程序一直输出0或者毫无规律的数值。

PerformanceCounter基本工作原理与核心属性
PerformanceCounter位于System.Diagnostics命名空间,它本质上是对Windows性能数据提供者(PDH)的一个托管封装。当我们创建一个实例时,需要指定三个关键参数:类别名(CategoryName)、计数器名(CounterName)以及可选的实例名(InstanceName)。对于整体CPU占用率,类别名固定为Processor,计数器名为% Processor Time,实例名用通配符_Total表示所有核心的合计值。
很多人忽略的一点是,性能计数器并不是实时快照。Windows以一定频率采样并更新内核中的计数器数据,而PerformanceCounter.NextValue方法返回的是自上次调用以来该计数器变化的折算值。因此第一次调用NextValue时,由于之前没有基准,往往返回0。只有间隔至少一秒再次调用,才能算出这段时间内的平均占用。这个设计决定了任何正确的CPU占用率读取都必须包含两次采样。
除了上述三个属性,MachineName允许指向远程机器,但需要确保权限与防火墙配置。ReadOnly属性一般保持为true即可,因为监控不需要写回计数器。在.NET Framework中这些类型开箱即用,但在.NET Core 3.0之后,Windows专属的PerformanceCounter仍然可用,只是必须运行在Windows系统上,跨平台编译时会提示不支持。
using System;
using System.Diagnostics;
class Demo
{
static void Main()
{
// 创建整体CPU计数器
PerformanceCounter cpuCounter = new PerformanceCounter(
"Processor", // 类别
"% Processor Time", // 计数器
"_Total"); // 实例,合计所有核心
// 第一次调用通常返回0
float first = cpuCounter.NextValue();
Console.WriteLine("首次读取: " + first);
// 等待一秒让系统采样
System.Threading.Thread.Sleep(1000);
// 第二次读取才是真实占用率
float second = cpuCounter.NextValue();
Console.WriteLine("一秒后CPU占用: " + second + "%");
}
}
获取单进程与多核CPU占用的代码实践
除了整机CPU,很多时候我们要监控的是某个进程的CPU消耗。此时类别名变为Process,计数器名仍是% Processor Time,但实例名必须是进程名(不带exe后缀)或者进程ID对应的实例。如果系统里同名进程有多个,PerformanceCounter会自动附加编号,例如chrome#1、chrome#2,这要求我们在代码里用Process.GetProcessById配合InstanceName的查找来匹配。
多核机器的单进程占用率有个坑:任务管理器里看到的进程CPU百分比是基于所有逻辑处理器之和的,而PerformanceCounter返回的值在多线程满负荷时可能超过100%。如果需要和任务管理器一致,应该除以逻辑核心数。下面示例展示如何读取指定进程名的平均占用,并做了简单的容错。
对于多实例场景,可以使用PerformanceCounterCategory.GetInstanceNames拿到全部实例,再逐一构造计数器。这种写法在写资源监视器类工具时很常见,但要控制采样频率,否则频繁创建对象会带来自身开销,反而干扰监控准确性。
using System;
using System.Diagnostics;
class ProcessCpu
{
static void Main()
{
string procName = "dotnet";
// 获取该进程类别下的所有实例名
var cat = new PerformanceCounterCategory("Process");
string[] instances = cat.GetInstanceNames();
foreach (var inst in instances)
{
if (inst.StartsWith(procName))
{
PerformanceCounter pc = new PerformanceCounter(
"Process", "% Processor Time", inst);
pc.NextValue();
System.Threading.Thread.Sleep(500);
float val = pc.NextValue();
// 除以逻辑核心数得到类似任务管理器的百分比
int cores = Environment.ProcessorCount;
Console.WriteLine(inst + " 占用: " + (val / cores) + "%");
}
}
}
}
常见误区、异常处理与跨平台替代方案
第一个典型误区是只调用一次NextValue就当作当前值,结果界面上永远显示0,用户以为代码失效。第二个误区是采样间隔太短,比如sleep 10毫秒,这样计数器还没更新,第二次值也接近0,曲线变成噪声。官方建议最短间隔200毫秒,实际监控用500毫秒到1秒最平稳。
异常处理方面,如果指定的进程已退出,PerformanceCounter会抛出InvalidOperationException。在生产代码里应该用try-catch包裹,并在界面层做降级显示。另外某些受限环境(如普通用户权限的共享主机)可能禁止访问性能计数器,此时要考虑改用WMI或者Task Manager提供的接口,不过那些方案复杂度更高。
跨平台需求现在越来越普遍。PerformanceCounter只在Windows实现,在Linux或macOS上构造会抛PlatformNotSupportedException。如果项目需要跨平台监控,应该抽象出接口,在Windows下用PerformanceCounter,在Linux下读取/proc/stat文件计算差值,在macOS下调用sysctl或ps命令。这样上层业务逻辑不用关心底层差异,也避免了部署到容器里直接崩溃。
using System;
using System.Diagnostics;
class SafeCpu
{
static void Main()
{
PerformanceCounter counter = null;
try
{
counter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
counter.NextValue();
System.Threading.Thread.Sleep(1000);
float v = counter.NextValue();
Console.WriteLine("安全读取CPU: " + v + "%");
}
catch (PlatformNotSupportedException)
{
Console.WriteLine("当前系统不支持PerformanceCounter,请使用跨平台方案");
}
catch (InvalidOperationException ex)
{
Console.WriteLine("计数器访问失败: " + ex.Message);
}
finally
{
counter?.Dispose();
}
}
}
把以上几部分组合起来,一个健壮的C# CPU占用率监控模块就清晰了。重点记住两次采样、合理间隔、实例名匹配以及平台限制,就能避开绝大多数坑。对于长期运行的服务,建议把计数器对象缓存复用,而不是每次读取都新建,既减少资源消耗也避免计数器泄漏。
C#PerformanceCounterCPU占用率修改时间:2026-08-18 22:18:38