异步/并行编程中持续运行任务的异常处理与重启方案问询
这类持续运行的异步/并行任务完全可行,而且是IoT监控场景下的常见模式。我来帮你解决异常捕获、自动重启的问题,同时优化你的代码并分享一些最佳实践。
1. 核心解决方案:异常捕获与自动重启的通用包装器
你需要一个通用的方法来包裹任务逻辑,负责处理异常、记录日志,并在任务失败时自动重启。这个包装器可以复用在所有持续运行的任务上:
private static async Task RunContinuousTask(Func<CancellationToken, Task> taskLogic, TimeSpan interval, CancellationToken cancellationToken, TimeSpan restartDelay = default) { if (restartDelay == default) restartDelay = TimeSpan.FromSeconds(5); // 默认失败后延迟5秒重启 while (!cancellationToken.IsCancellationRequested) { try { await taskLogic(cancellationToken); await Task.Delay(interval, cancellationToken); // 任务正常完成后等待间隔时间 } catch (OperationCanceledException) { // 任务被主动取消,退出循环 Console.WriteLine("任务已被取消,退出执行"); break; } catch (Exception ex) { // 记录异常信息 Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 任务执行失败: {ex.GetType().Name} - {ex.Message}\n堆栈跟踪: {ex.StackTrace}"); // 等待重启延迟,避免频繁重启 await Task.Delay(restartDelay, cancellationToken); } } }
这个方法的作用是:
- 循环执行传入的任务逻辑
- 捕获非取消类异常,记录日志后延迟重启
- 处理取消令牌,收到取消信号后优雅退出
2. 优化你的任务逻辑
现在把你的三个任务改成符合异步规范的逻辑,并使用上面的包装器启动:
优化后的完整代码
using System; using System.Threading; using System.Threading.Tasks; namespace taskTest { public class Program { public static async Task Main(string[] args) { // 使用一个统一的取消令牌源,方便整体管理所有任务的取消 using var cts = new CancellationTokenSource(); // 启动三个持续任务,使用统一的取消令牌 var task1 = RunContinuousTask(ExecuteSensorPolling, TimeSpan.FromSeconds(1), cts.Token); var task2 = RunContinuousTask(ExecuteServerHealthCheck, TimeSpan.FromSeconds(10), cts.Token); var task3 = RunContinuousTask(ExecuteSensorListSync, TimeSpan.FromMinutes(1), cts.Token); Console.WriteLine("所有任务已启动,按任意键停止..."); Console.ReadLine(); // 发送取消信号,停止所有任务 cts.Cancel(); await Task.WhenAll(task1, task2, task3); Console.WriteLine("所有任务已停止"); } #region 任务逻辑实现 /// <summary> /// 每1秒轮询传感器状态 /// </summary> private static async Task ExecuteSensorPolling(CancellationToken cancellationToken) { // 这里替换成你的SOAP API调用逻辑 Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 轮询传感器状态..."); // 模拟API调用延迟 await Task.Delay(500, cancellationToken); } /// <summary> /// 每10秒检查Web服务器在线状态 /// </summary> private static async Task ExecuteServerHealthCheck(CancellationToken cancellationToken) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 检查服务器在线状态..."); // 模拟可能抛出的异常 if (DateTime.Now.Second % 30 == 0) // 每30秒模拟一次异常 { throw new InvalidOperationException("服务器连接超时"); } await Task.Delay(200, cancellationToken); } /// <summary> /// 每分钟同步传感器列表(新增/删除) /// </summary> private static async Task ExecuteSensorListSync(CancellationToken cancellationToken) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 同步传感器列表..."); await Task.Delay(1000, cancellationToken); } #endregion #region 通用持续任务包装器 private static async Task RunContinuousTask(Func<CancellationToken, Task> taskLogic, TimeSpan interval, CancellationToken cancellationToken, TimeSpan restartDelay = default) { if (restartDelay == default) restartDelay = TimeSpan.FromSeconds(5); while (!cancellationToken.IsCancellationRequested) { try { await taskLogic(cancellationToken); await Task.Delay(interval, cancellationToken); } catch (OperationCanceledException) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 任务已取消"); break; } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 任务执行失败,将在{restartDelay.TotalSeconds}秒后重启:"); Console.WriteLine($"异常类型: {ex.GetType().Name}"); Console.WriteLine($"异常消息: {ex.Message}"); Console.WriteLine($"堆栈跟踪: {ex.StackTrace}\n"); try { await Task.Delay(restartDelay, cancellationToken); } catch (OperationCanceledException) { // 等待重启期间收到取消信号,直接退出 break; } } } } #endregion } }
3. 原代码中的问题纠正
- 同步循环导致CPU空转:原Task1中
Task.Delay(1000)没有用await,会导致循环瞬间执行,占用大量CPU。改成异步后用await可以让线程在等待期间释放,提升性能。 - 未处理的异常:原Task2的catch块是空的,无法排查问题;原Task3如果抛出异常会变成未观察到的异常(可能导致进程崩溃),现在统一在包装器中处理。
- 分散的取消令牌:原代码用了三个独立的
CancellationTokenSource,不利于统一管理任务的启动和停止,现在用一个统一的源,所有任务共享同一个令牌。 - 任务启动方式不规范:原Task1和Task2是同步方法直接调用,没有用Task.Run或异步启动,会阻塞主线程(虽然原代码有Console.ReadLine,但长期运行的程序应该避免同步阻塞)。
4. 最佳实践建议
- 优先使用异步/await:所有IO密集型操作(比如调用SOAP API、网络请求)都应该用异步方法,避免阻塞线程池线程。
- 统一取消令牌管理:用一个
CancellationTokenSource管理所有相关任务,确保可以优雅地停止所有任务,避免资源泄漏。 - 完善的异常日志:不要只记录异常消息,还要记录异常类型、堆栈跟踪、发生时间,方便后续排查问题。如果是生产环境,建议使用专业日志框架(如Serilog、NLog)代替Console.WriteLine。
- 合理的重启策略:异常后不要立即重启,加一个延迟(比如5秒),避免因网络波动等临时问题导致任务频繁重启,消耗资源。
- 任务幂等性:确保任务逻辑是幂等的,即重复执行不会产生不良影响(比如轮询传感器状态、检查服务器健康都是幂等的)。
- 避免未观察到的异常:所有Task的异常都必须被处理,要么通过
await捕获,要么通过ContinueWith处理,否则在旧版本的.NET中可能导致进程崩溃。
内容的提问来源于stack exchange,提问作者technologyresearcher
相关产品推荐
相关产品推荐

