C#插件开发中无限while循环如何避免内存泄漏
插件无限循环的内存泄漏问题排查与优化
核心认知澄清
你当前代码最容易引发内存泄漏的根源不是循环本身的业务逻辑,而是没有退出机制的永久循环强引用,是否使用using、是否封装类只是优化手段,要逐层拆解问题:
潜在的内存泄漏风险点
- 线程生命周期失控:当前
while(true)没有任何终止条件,如果宿主应用卸载插件时无法主动中断这个循环线程,线程作为GC根对象会强持有整个插件程序集的所有引用,导致GC完全无法回收插件加载的所有类型、静态变量、大字典数据,这是插件类程序最常见的泄漏场景。 - 重复无意义的对象创建:每次循环都从
vaProxy.SessionState强转字典、做dynamic类型绑定,虽然这些临时字典的引用在循环迭代后就会消失,正常会被GC回收,但如果运行时动态绑定没有做缓存,会生成大量动态调用的临时缓存对象,长时间运行会持续占用托管堆内存。 - 非托管句柄泄漏:代码里的
SharedMem是访问游戏共享内存的模块,如果它内部每次取状态都重新打开内存映射文件、申请内存视图而不主动关闭句柄,会持续消耗系统非托管内存,很容易被误判成循环逻辑导致的泄漏。
关于using语句的使用说明
不要在整个无限循环外面套using,这是完全无效甚至有反作用的写法:
using的核心作用是在代码执行到作用域末尾时,自动调用对象的IDisposable.Dispose()释放非托管资源,而你的循环进入后就永远不会走到作用域末尾,哪怕包了using,Dispose方法永远不会被执行,没有任何实际作用。- 绝对不要对
vaProxy使用using:这个对象是宿主应用控制的,生命周期由宿主管理,你一旦Dispose掉它,后续循环里所有对它的调用都会直接报错。 - 循环内正确使用
using的场景:如果业务逻辑里需要创建临时的实现IDisposable的对象(比如临时打开的共享内存访问器、临时文件读取流),就在当前单次循环的作用域内用using包住,保证单次操作完成立刻释放非托管资源,示例:// 单次循环内的非托管资源用完即放 using (var sharedMemAccessor = SharedMem.CreateAccessor()) { var isFlying = sharedMemAccessor.GetStatus("isFlying"); // 操作完立刻释放句柄 }
业务逻辑封装为类的优化效果
把循环逻辑封装成类是非常推荐的做法,能从结构上降低内存泄漏风险,具体收益:
可以集中管理资源的生命周期:把循环里反复用到的对象(比如回调字典、配置读取器、共享内存访问器)在类初始化时一次性缓存,不要每轮循环都重复从SessionState取值、重复做类型转换,减少临时对象的生成量。
可以方便实现可终止的循环逻辑:在类内维护取消令牌,当宿主触发插件卸载时,主动中断循环、释放所有持有的引用,从根源解决线程强引用导致的程序集无法回收的问题。
方便统一排查资源释放问题:所有需要Dispose的资源都可以在类的
Dispose方法里统一释放,不会散落在循环代码里漏写。一个基础的封装结构参考:
public class DynamicCheckLoop : IDisposable { private readonly IVAProxy _proxy; private readonly CancellationTokenSource _cts = new CancellationTokenSource(); // 缓存重复使用的对象,避免每轮循环重复取值 private readonly Dictionary<string, CallBack> _cachedCallbacks; private const int SleepInterval = 60 * 1000; public DynamicCheckLoop(IVAProxy proxy) { _proxy = proxy; // 初始化时一次性取好回调字典的引用 _cachedCallbacks = proxy.SessionState["CallBacks"] as Dictionary<string, CallBack>; } public void Start() { Task.Run(async () => { // 循环判断取消状态,替代永真的while(true) while (!_cts.IsCancellationRequested) { try { await Task.Delay(SleepInterval, _cts.Token); RunBusinessChecks(); } catch (OperationCanceledException) { // 收到取消信号,正常退出循环 break; } catch (Exception ex) { _proxy.WriteToLog($"Dynamic Checks\n{ex.Message}", "Red"); Console.Beep(); } } }, _cts.Token); } private void RunBusinessChecks() { // 把原循环里的业务逻辑迁移到这里,复用缓存的_cachedCallbacks、_proxy等对象 } public void Dispose() { // 插件卸载时触发,彻底停止循环、释放资源 _cts.Cancel(); _cts.Dispose(); } }
最终的泄漏规避清单
- 替换无终止的
while(true)为可通过取消令牌中断的循环逻辑,保证插件卸载时能正确退出循环线程。 - 把每轮循环重复获取的字典、回调引用缓存到类成员中,减少重复类型转换、动态绑定带来的临时对象开销。
- 排查共享内存模块的句柄释放逻辑,确认非托管资源没有逐次循环累积泄漏。
- 不要在循环内持有大对象的静态引用,保证循环迭代过程中产生的临时对象能被GC正常回收。
内容的提问来源于stack exchange,提问作者dandan
相关产品推荐
相关产品推荐

