C# Windows服务Timer触发及OnStop时异步任务无法日志记录求助
问题根源分析
你的问题核心出在**async void的不当使用**以及Windows服务中异步任务的生命周期管理上:
async void的风险:这种异步方法仅适合UI事件处理场景,在Windows服务这类非UI环境中,异常会直接抛到同步上下文(可能导致服务崩溃),且无法跟踪任务的完成状态——服务停止时异步任务可能还没执行完就被终止,Timer触发的任务异常会被静默吞掉,直接导致日志无输出。- Timer回调的异步处理漏洞:
System.Timers.Timer的Elapsed事件在线程池线程触发,使用async void时,任务执行完成后没有上下文关联,中途若出现异常会直接丢失,自然看不到日志。 - OnStop的同步限制:OnStop是同步方法,调用
async void会立即返回,服务可能在异步日志任务完成前就停止,所以停止日志根本没机会写入。
解决方案
1. 重构异步日志方法为async Task
首先把WriteLog改成可等待的异步方法,并添加异常捕获,避免异常静默丢失:
// 用于跟踪所有正在运行的异步日志任务 private readonly ConcurrentBag<Task> _runningTasks = new ConcurrentBag<Task>(); private async Task WriteLogAsync(WorkerLog worker) { try { string x = await Logger.WriteLogAsync(worker); Logger.WriteError(x); } catch (Exception ex) { // 捕获并记录异常,避免任务失败无痕迹 Logger.WriteError($"日志写入失败: {ex.Message}\n{ex.StackTrace}"); } }
2. 正确处理Timer的异步回调
如果使用System.Timers.Timer,修改Elapsed事件处理程序为async void(这是事件处理的特例),内部调用重构后的异步方法并跟踪任务状态:
private System.Timers.Timer _timer; protected override void OnStart(string[] args) { // 初始化Timer,30秒触发一次 _timer = new System.Timers.Timer(30000); _timer.Elapsed += async (sender, e) => await HandleTimerElapsed(); _timer.Start(); // 同步等待启动日志写入完成,确保启动日志能正常输出 var startLog = new WorkerLog { /* 填充你的启动日志内容 */ }; WriteLogAsync(startLog).GetAwaiter().GetResult(); } private async Task HandleTimerElapsed() { var timerLog = new WorkerLog { /* 填充Timer触发的日志内容 */ }; var task = WriteLogAsync(timerLog); _runningTasks.Add(task); // 任务完成后从跟踪集合中移除 await task.ContinueWith(t => { _runningTasks.TryTake(out _); }, TaskContinuationOptions.ExecuteSynchronously); }
3. 让OnStop等待异步任务完成
OnStop需要先停止Timer,再等待所有正在运行的日志任务完成,最后同步写入停止日志:
protected override void OnStop() { // 先停止Timer,避免新的异步任务触发 _timer?.Stop(); _timer?.Dispose(); // 等待所有正在运行的日志任务完成 var runningTasks = _runningTasks.ToArray(); if (runningTasks.Length > 0) { Task.WaitAll(runningTasks); } // 同步写入停止日志,确保服务停止前日志写完 var stopLog = new WorkerLog { /* 填充你的停止日志内容 */ }; WriteLogAsync(stopLog).GetAwaiter().GetResult(); }
额外注意事项
- Logger的线程安全性:确保
Logger.WriteLogAsync和Logger.WriteError是线程安全的,因为Timer回调、OnStart/OnStop会在不同线程调用这些方法,线程不安全可能导致日志丢失或异常。 - 避免OnStart长时间阻塞:如果
WriteLogAsync执行时间很长,GetAwaiter().GetResult()会阻塞OnStart,可能导致服务启动超时。这种情况下可以把启动日志放到后台任务,但要将任务加入_runningTasks跟踪,确保能完成。
内容的提问来源于stack exchange,提问作者Tajuddin Khandaker
相关产品推荐
相关产品推荐

