C# Timer线程随机运行后抛出Thread was being aborted异常排查
故障根因
"Thread was being aborted." 对应CLR抛出的ThreadAbortException,这个异常不会被普通业务代码无意触发,只有两种触发可能:
- 代码路径中存在主动调用
Thread.Abort()的逻辑 - 当前代码运行所在的AppDomain被卸载,对应你这个MVC项目的场景,就是IIS应用池回收、站点目录文件变更触发重启、内存占用超阈值触发回收这类宿主生命周期事件,这也是故障触发时间完全随机的核心原因——和你轮询的业务逻辑没有直接关系。
你现有代码里的几个设计缺陷,会把偶发的宿主生命周期事件变成线程永久停跑的故障:
PollingIfood方法用了错误的async void签名:方法内部没有任何await异步调用,加async关键字属于多余写法。System.Threading.Timer的回调本身是跑在线程池工作线程上的同步委托,async void会导致回调内的异常逃逸出Timer的执行上下文,一旦抛出异常直接打断执行流,没有兜底机会。- 静态Timer的生命周期管理有漏洞:Timer实例是静态字段初始化的,但
Reiniciar()方法重置单例的时候,没有主动Dispose旧的Timer实例,旧Timer要么被GC回收后停止触发,要么和新创建的Timer产生执行冲突。 - 没有针对
ThreadAbortException做特殊处理:这个异常是CLR特殊处理的异常,哪怕被catch块捕获,catch执行完后CLR会自动重新抛出,你现在的catch只打日志,没有调用Thread.ResetAbort()取消中止标记,会导致finally块里重置Timer的逻辑执行被打断,Timer直接停在无限等待状态,再也不会触发下一轮轮询。 - 没有适配IIS的应用生命周期:ASP.NET跑在IIS上时,默认就有空闲超时回收、固定时间回收、文件变更触发重启的机制,你没有注册AppDomain卸载的监听事件,完全感知不到宿主即将关闭的信号,只能等线程被强制Abort。
修复方案
按以下步骤调整即可解决问题:
- 去掉
PollingIfood的async修饰符,匹配Timer原生回调签名,避免async void的异常逃逸问题。 - 修正单例重置逻辑:
Reiniciar()方法执行时先Dispose旧的Timer实例,再重置状态,避免产生游离的Timer对象。 - 异常捕获单独处理
ThreadAbortException,捕获后调用Thread.ResetAbort()取消CLR的线程中止标记,保证finally块的Timer重置逻辑能正常执行。 - 调整Timer初始化时机:不要在静态字段声明时直接创建Timer,放到
Inicializa()方法里加锁初始化,避免静态构造时机不确定带来的异常。 - 注册AppDomain的
DomainUnload事件,感知宿主卸载动作,主动释放Timer资源。 - 调整IIS站点配置:将应用池空闲超时设为0,开启应用池「始终运行」模式、站点预加载,把应用池固定回收时间设置到业务低峰期,减少随机回收的概率。
修正后的核心代码如下:
using System.Threading; namespace NewMVC.infraestructure { public class Thread_iFood { private static bool inicializado = false; static Thread_iFood instance = null; static readonly object padlock = new object(); private const int timer = 30000; static Timer thProcessaRet = null; private Thread_iFood() { // 监听应用域卸载事件,提前感知宿主回收 AppDomain.CurrentDomain.DomainUnload += (s, e) => { thProcessaRet?.Dispose(); thProcessaRet = null; }; } public static void Reiniciar() { lock (padlock) { thProcessaRet?.Dispose(); thProcessaRet = null; inicializado = false; instance = null; } } public static Thread_iFood GetInstance { get { if (instance == null) { lock (padlock) { instance ??= new Thread_iFood(); } } return instance; } } // 移除async关键字,匹配TimerCallback委托签名 internal static void PollingIfood(Object obj) { try { thProcessaRet.Change(Timeout.Infinite, Timeout.Infinite); MetodosAuxiliares.BuscaPedidosIFood(); } catch (ThreadAbortException tae) { // 重置线程中止标记,阻止CLR自动重抛异常 Thread.ResetAbort(); LogErros.GravaLog(tae, "Thread_iFood/PollingIfood 触发线程中止,已重置标记"); } catch (Exception e) { LogErros.GravaLog(e, "Thread_iFood/PollingIfood"); } finally { thProcessaRet?.Change(timer, Timeout.Infinite); } } public void Inicializa() { if (!inicializado) { lock (padlock) { if (!inicializado) { thProcessaRet ??= new Timer(PollingIfood, null, Timeout.Infinite, Timeout.Infinite); thProcessaRet.Change(timer, Timeout.Infinite); inicializado = true; } } } } } }
注意:IIS本身是为处理HTTP请求设计的宿主,天生存在应用回收机制,如果你的轮询任务是核心业务链路,最稳妥的方案是把轮询逻辑单独拆成Windows服务或者独立后台服务部署,不要直接挂在MVC站点的进程里跑。
内容的提问来源于stack exchange,提问作者MOGO SMART
相关产品推荐
相关产品推荐

