C#中基于System.Timers.Timer的Windows服务:如何检测前一次任务是否仍在运行及获取线程CPU内存占用
解决Windows服务中Timer任务的运行状态判断与资源监控问题
我来帮你拆解这两个常见的Windows服务场景问题,咱们一步步来实现:
一、判断前一次任务是否仍在运行
要避免Timer触发时前一次长任务还没执行完,最稳妥的方式是用线程安全的状态标记来控制,这里有两种常见实现方式:
1. 基于锁和布尔标记(同步任务场景)
适合你的长任务是同步执行的情况,用锁保证状态标记的线程安全:
private System.Timers.Timer _taskTimer; private readonly object _lockObj = new object(); private bool _isTaskInProgress; // 服务启动时初始化Timer public void StartMyService() { int intervalMinutes = X; // 你的间隔分钟数 _taskTimer = new System.Timers.Timer(intervalMinutes * 60 * 1000); _taskTimer.Elapsed += OnTaskTimerElapsed; _taskTimer.Start(); } private void OnTaskTimerElapsed(object sender, System.Timers.ElapsedEventArgs e) { // 先加锁检查任务状态 lock (_lockObj) { if (_isTaskInProgress) { // 前一次任务还在跑,直接跳过本次触发 return; } _isTaskInProgress = true; } try { // 执行你的长时间运行操作 ExecuteLongRunningTask(); } catch (Exception ex) { // 记得在这里记录异常日志,避免任务崩溃导致状态标记一直为true // LogHelper.Error("长任务执行失败", ex); } finally { // 无论任务成功/失败,都重置状态标记 lock (_lockObj) { _isTaskInProgress = false; } } } private void ExecuteLongRunningTask() { // 你的业务逻辑代码 }
2. 基于SemaphoreSlim(异步任务场景)
如果你的长任务是异步的(用async/await),锁里不能直接await,这时用SemaphoreSlim更合适:
private System.Timers.Timer _taskTimer; private readonly SemaphoreSlim _taskSemaphore = new SemaphoreSlim(1, 1); public void StartMyService() { int intervalMinutes = X; _taskTimer = new System.Timers.Timer(intervalMinutes * 60 * 1000); _taskTimer.Elapsed += OnTaskTimerElapsedAsync; _taskTimer.Start(); } // 注意这里用async void(因为Timer的Elapsed事件是void返回) private async void OnTaskTimerElapsedAsync(object sender, System.Timers.ElapsedEventArgs e) { // 尝试获取信号量,0毫秒超时表示直接检查是否能进入 if (!await _taskSemaphore.WaitAsync(0)) { return; } try { await ExecuteLongRunningTaskAsync(); } catch (Exception ex) { // 异常日志记录 } finally { _taskSemaphore.Release(); } } private async Task ExecuteLongRunningTaskAsync() { // 你的异步业务逻辑 }
二、获取运行中任务线程的CPU和内存资源情况
要追踪线程的资源,首先得记录任务执行的线程ID,然后结合Process类或性能计数器来获取数据:
1. 记录任务线程ID
在任务执行的代码里,实时获取当前运行的线程ID:
private int _currentTaskThreadId; private void ExecuteLongRunningTask() { // 记录当前线程ID _currentTaskThreadId = Thread.CurrentThread.ManagedThreadId; // 业务逻辑... } // 异步版本 private async Task ExecuteLongRunningTaskAsync() { // 注意:异步任务可能会切换线程,这里记录的是初始执行的线程ID _currentTaskThreadId = Thread.CurrentThread.ManagedThreadId; // 异步业务逻辑... }
2. 获取线程CPU使用率
ProcessThread类可以获取线程的累计CPU时间,通过两次采样计算差值得到实时使用率:
private DateTime _lastCpuSampleTime; private TimeSpan _lastTotalCpuTime; public double GetCurrentTaskCpuUsage() { lock (_lockObj) { if (!_isTaskInProgress) { return 0; } } var currentProcess = Process.GetCurrentProcess(); // 根据线程ID找到对应的ProcessThread var targetThread = currentProcess.Threads.Cast<ProcessThread>() .FirstOrDefault(t => t.Id == _currentTaskThreadId); if (targetThread == null) { return 0; } var currentTime = DateTime.Now; var currentTotalCpuTime = targetThread.UserProcessorTime + targetThread.PrivilegedProcessorTime; // 第一次采样初始化 if (_lastCpuSampleTime == default) { _lastCpuSampleTime = currentTime; _lastTotalCpuTime = currentTotalCpuTime; return 0; } // 计算时间差和CPU使用时间差 double elapsedSeconds = (currentTime - _lastCpuSampleTime).TotalSeconds; double cpuUsedSeconds = (currentTotalCpuTime - _lastTotalCpuTime).TotalSeconds; // 单个线程的CPU使用率上限是100%(对应一个核心) double cpuUsage = Math.Min((cpuUsedSeconds / elapsedSeconds) * 100, 100); // 更新采样记录 _lastCpuSampleTime = currentTime; _lastTotalCpuTime = currentTotalCpuTime; return cpuUsage; }
3. 内存资源追踪
线程本身的内存主要是堆栈,很难精确获取实际使用量,通常我们关注以下两类内存:
- 进程级内存:用
Process类获取当前进程的内存使用:var currentProcess = Process.GetCurrentProcess(); long privateMemoryBytes = currentProcess.PrivateMemorySize64; // 私有内存(单位:字节) long workingSetBytes = currentProcess.WorkingSet64; // 工作集内存(单位:字节) - .NET托管堆内存:获取应用程序的托管内存使用:
long managedHeapMemory = GC.GetTotalMemory(false); // 当前托管堆已分配内存(单位:字节)
注意事项
- 如果你的任务是用线程池线程执行的(比如
Task.Run),线程ID可能会被复用,所以最好在任务内部实时记录线程ID,而不是在Timer事件里。 - 异步任务可能会切换线程,所以线程ID只能追踪初始执行的线程,后续切换的线程无法通过ID关联,这种场景下建议用性能分析工具(如Visual Studio性能探查器)来监控。
内容的提问来源于stack exchange,提问作者enfix
相关产品推荐
相关产品推荐

