在Windows平台上监控系统性能指标,最直接的方式就是读取性能计数器。C#提供了PerformanceCounter类,把原本需要通过注册表或PDH接口才能获取的数据封装成了简单的对象模型。无论是CPU使用率、内存占用还是磁盘I/O,只要知道计数器路径,就能在几行代码内拿到实时数值。不过这个类在使用时有不少细节容易被忽略,比如特定计数器需要两次采样、权限不足会抛出异常、频繁创建实例会带来明显开销。下面用获取CPU和内存使用率作为主线,把PerformanceCounter的用法和常见陷阱梳理清楚。

PerformanceCounter类基础
PerformanceCounter类位于System.Diagnostics命名空间,核心构造方法接收三个关键参数:类别名称、计数器名称和实例名称。比如CPU总使用率的类别是Processor,计数器是% Processor Time,实例通常填_Total代表所有核心的平均值。创建对象后调用NextValue方法即可返回当前值,返回类型是float。但需要注意,并非所有计数器都返回百分比或直观的物理量,有些返回原始累计值,需要自行计算差值;有些则直接返回计算后的百分比。
下面的代码创建了一个针对CPU总使用率的计数器。第一次调用NextValue时返回0或很小的值,这是正常现象,因为该计数器本质上记录的是自上次采样以来的平均忙碌时间占比。如果立刻取值,系统还没有足够的间隔数据,结果自然不可用。正确做法是先调用一次“预热”,然后让线程等待至少500毫秒到1秒,再调用第二次,此时得到的是这段时间内的平均CPU占用率。
using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main()
{
var cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
cpuCounter.NextValue();
Thread.Sleep(1000);
float cpuUsage = cpuCounter.NextValue();
Console.WriteLine($"CPU使用率: {cpuUsage}%");
}
}
构造PerformanceCounter时如果类别、计数器或实例名写错,会抛出InvalidOperationException。例如把Processor误写成Processer,或者实例名_Total写成Total,都会导致运行时错误。在正式项目里建议把计数器路径集中管理,并在创建实例时用try-catch捕获异常,防止因环境差异导致程序崩溃。另外,性能计数器的类别名称和实例名受系统语言影响,中文Windows上某些类别可能显示为本地化名称,但使用英文原名通常仍然有效,因为底层PDH库内部维护了映射。
获取CPU使用率
获取CPU总使用率的标准路径是Processor类别下的% Processor Time计数器,实例_Total。这个计数器返回0到100之间的浮点数,表示所有逻辑处理器的平均忙碌百分比。对于多核CPU,如果希望知道每一个核心的使用情况,可以把实例名替换成0、1、2等索引,分别对应每个逻辑处理器。实例名的总数可以通过读取Processor类别的实例列表获得,使用PerformanceCounterCategory.GetCategories和ReadCategory可以枚举。
两次采样之间的等待时间直接影响结果的平滑程度。等待时间太短,比如100毫秒,得到的数值波动会很大,几乎没什么参考意义;等待时间太长,比如10秒,虽然数值稳定,但实时性大打折扣。一般监控场景下1秒到2秒是比较合适的折中。如果只是偶尔读取一次,可以简化成每次创建计数器、采样两次后立刻释放,但频繁这样做会拖慢性能,因为每个计数器实例的创建和销毁都会与系统性能库交互。
除了使用PerformanceCounter,还可以通过Process类获取当前进程的CPU时间,再结合系统总运行时间估算CPU占用,但这种方式只能反映单个进程,无法得到整机数据。相比之下,PerformanceCounter更适合系统级监控。需要注意的是,% Processor Time在部分旧系统或虚拟机中可能存在返回值超过100的情况,这通常是因为多个核心的时间叠加或虚拟化环境的时间同步问题,实际使用时可以做上限截断处理。
using System;
using System.Diagnostics;
using System.Threading;
class CpuMonitor
{
private PerformanceCounter cpuCounter;
public CpuMonitor()
{
cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
cpuCounter.NextValue();
}
public float GetCpuUsage()
{
Thread.Sleep(1000);
float value = cpuCounter.NextValue();
return Math.Min(value, 100f); // 防止异常值超过100
}
}
class Program
{
static void Main()
{
var monitor = new CpuMonitor();
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"第{i + 1}次采样 CPU使用率: {monitor.GetCpuUsage():F1}%");
}
}
}
上面代码把计数器实例保持在对象中复用,避免每次都重新创建。同时把NextValue的调用和等待逻辑封装成单独方法,便于多次采集。实际项目中如果还需要监控磁盘、网络等指标,可以创建多个计数器对象,但要注意每个计数器都会占用一个系统句柄,数量过多时需要及时调用Dispose方法释放资源。PerformanceCounter实现了IDisposable接口,用using语句包裹是最安全的方式。
获取内存使用率
内存使用率通常有两种计算方式:一种是读取可用内存,然后用总内存减去可用内存得到已用内存,再除以总内存;另一种是直接使用Memory类别下的% Committed Bytes In Use计数器。前者需要依赖系统总内存数值,在32位进程中读取大于4GB的内存信息可能失败;后者直接给出百分比,更简单但有轻微误差,因为它统计的是“已提交字节”而不是物理内存占用,提交字节包括页面文件,所以数值可能超过物理内存使用率。
先看可用内存的方法。Memory类别下的Available MBytes计数器返回当前可用的物理内存,单位是MB。总内存可以通过Microsoft.VisualBasic.Devices.ComputerInfo类获得,它的TotalPhysicalMemory属性返回一个ulong,单位是字节。把字节转换成MB后,就可以计算使用率。下面代码演示了这种组合方式,并处理了32位环境可能出现的溢出问题。
using System;
using System.Diagnostics;
using Microsoft.VisualBasic.Devices;
class MemoryMonitor
{
private PerformanceCounter availableCounter;
private ComputerInfo computerInfo;
public MemoryMonitor()
{
availableCounter = new PerformanceCounter("Memory", "Available MBytes");
computerInfo = new ComputerInfo();
}
public float GetMemoryUsage()
{
float availableMB = availableCounter.NextValue();
ulong totalBytes = computerInfo.TotalPhysicalMemory;
float totalMB = totalBytes / (1024f * 1024f);
float usedPercent = (totalMB - availableMB) / totalMB * 100;
return Math.Max(0, Math.Min(usedPercent, 100f));
}
}
class Program
{
static void Main()
{
var monitor = new MemoryMonitor();
for (int i = 0; i < 3; i++)
{
Console.WriteLine($"内存使用率: {monitor.GetMemoryUsage():F1}%");
System.Threading.Thread.Sleep(2000);
}
}
}
如果不想引入Microsoft.VisualBasic命名空间,也可以使用WMI查询Win32_ComputerSystem类的TotalPhysicalMemory属性,或者调用GlobalMemoryStatusEx这个Windows API。不过WMI查询相对较慢,适合低频调用;API方式代码较为繁琐,但性能最好。对于简单监控场景,Microsoft.VisualBasic.Devices.ComputerInfo已经足够,且它在.NET Framework和.NET Core(需要添加Microsoft.VisualBasic包)中都能使用。
另一个更直接的方法是使用% Committed Bytes In Use计数器。这个计数器路径为Memory、% Committed Bytes In Use,没有实例名,返回0到100的百分比。它的含义是当前已提交字节数占提交上限的比例,提交上限通常等于物理内存加页面文件大小。因此它反映的是系统整体的内存压力,而非纯粹的物理内存占用。在物理内存充足但页面文件紧张的情况下,这个值可能会偏高,但对大多数应用来说已经足够有参考价值。
using System;
using System.Diagnostics;
class Program
{
static void Main()
{
using var memCounter = new PerformanceCounter("Memory", "% Committed Bytes In Use");
float usage = memCounter.NextValue();
Console.WriteLine($"提交内存使用率: {usage:F1}%");
}
}
两种方法各有适用场景。如果需要精确的物理内存占用比例,应该采用可用内存加总内存的计算方式;如果更关心系统是否有内存压力、是否接近提交上限,% Committed Bytes In Use是更好的选择。实际开发中建议同时采集两个指标,并通过日志或图表观察它们的趋势差异。
注意事项与替代方案
使用PerformanceCounter需要注意权限问题。在Windows上读取性能计数器通常不需要管理员权限,但访问某些与安全或审计相关的类别可能需要提权。如果程序运行在IIS或服务进程中,账户权限可能受限,这时可以尝试将应用池标识改为LocalSystem或授予Performance Monitor Users组权限。另外,在非Windows平台上PerformanceCounter不可用,.NET Core的System.Diagnostics.PerformanceCounter包只在Windows下工作,跨平台项目需要条件编译或使用EventCounters等替代机制。
性能开销方面,每个PerformanceCounter实例在内部维护与系统性能库的连接,创建和销毁都有一定的成本。如果监控频率很高,比如每秒10次以上,建议复用计数器对象而不是每次重新new。采样本身的开销很小,但频繁的计数器枚举(比如遍历所有进程)会明显拖慢系统,需要控制频率。对于生产环境的监控系统,一般设计成后台线程定时采样,采样间隔不超过1秒,同时限制同时存在的计数器数量。
替代PerformanceCounter的方案包括:使用System.Diagnostics.Process类获取当前进程的WorkingSet64和TotalProcessorTime,但无法获得整机数据;使用WMI的Win32_Processor和Win32_OperatingSystem类,功能全面但速度较慢;调用Windows API GlobalMemoryStatusEx获取内存信息,代码稍复杂但性能极佳;在.NET 5及以上版本还可以使用System.Diagnostics.Metrics命名空间配合EventCounters,更适合应用内部指标采集。选择哪种方案取决于具体需求:如果只监控本机整机数据且运行在Windows上,PerformanceCounter仍然是最简单直接的选择。
最后需要提醒的是,PerformanceCounter读取的值受到虚拟机、容器和远程桌面环境影响。在Hyper-V或VMware中,CPU计数器的数值可能不准确;在Docker容器内,性能计数器可能无法读取宿主机数据。部署到生产环境前务必在目标环境中充分测试,并做好异常回退逻辑。
C#PerformanceCounterCPU使用率修改时间:2026-10-03 15:19:20