为什么在 C# 控制台应用程序中使用依赖注入时内存利用率不断增加?

Why is memory utilization continuiously increasing when using dependency injection in a C# console application?

我可能知道我的 posted 问题的答案:我在整个应用程序中使用构造函数依赖注入,这是一个循环的 C# 控制台应用程序,在每次请求后不会退出。

我怀疑所有包含的对象的生命周期因此基本上是无限的。在注册时尝试调整生命周期时,它会警告 t运行sient 对象由于依赖关系而无法在单例对象上实现(这启发了查看内存利用率和这个问题)。

这是我的第一个控制台应用程序,一个机器人,它登录到服务提供商并等待消息。我来自 .NET Core Web API,它再次具有依赖性,但我认为这里的关键区别在于我的所有代码下方是平台本身,它单独处理每个请求然后杀死 运行.

我有多接近?我是否必须将机器人本身与侦听服务提供商的基本控制台应用程序分开,并尝试复制 IIS/kestrel/MVC 路由提供的平台来分离各个请求?

编辑: 最初我打算将此问题更多地作为设计原则、最佳实践或寻求方向指导。人们要求可重现的代码,所以我们开始:

namespace BotLesson
{
    internal class Program
    {
        private static readonly Container Container;

        static Program()
        {
            Container = new Container();
        }

        private static void Main(string[] args)
        {
            var config = new Configuration(args);

            Container.AddConfiguration(args);
            Container.AddLogging(config);

            Container.Register<ITelegramBotClient>(() => new TelegramBotClient(config["TelegramToken"])
            {
                Timeout = TimeSpan.FromSeconds(30)
            });
            Container.Register<IBot, Bot>();
            Container.Register<ISignalHandler, SignalHandler>();

            Container.Register<IEventHandler, EventHandler>();
            Container.Register<IEvent, MessageEvent>();

            Container.Verify();

            Container.GetInstance<IBot>().Process();

            Container?.Dispose();
        }
    }
}

Bot.cs

namespace BotLesson
{
    internal class Bot : IBot
    {
        private readonly ITelegramBotClient _client;
        private readonly ISignalHandler _signalHandler;
        private bool _disposed;

        public Bot(ITelegramBotClient client, IEventHandler handler, ISignalHandler signalHandler)
        {
            _signalHandler = signalHandler;

            _client = client;
            _client.OnCallbackQuery += handler.OnCallbackQuery;
            _client.OnInlineQuery += handler.OnInlineQuery;
            _client.OnInlineResultChosen += handler.OnInlineResultChosen;
            _client.OnMessage += handler.OnMessage;
            _client.OnMessageEdited += handler.OnMessageEdited;
            _client.OnReceiveError += (sender, args) => Log.Error(args.ApiRequestException.Message, args.ApiRequestException);
            _client.OnReceiveGeneralError += (sender, args) => Log.Error(args.Exception.Message, args.Exception);
            _client.OnUpdate += handler.OnUpdate;
        }

        public void Process()
        {
            _signalHandler.Set();
            _client.StartReceiving();

            Log.Information("Application running");

            _signalHandler.Wait();

            Log.Information("Application shutting down");
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed) return;
            if (disposing) _client.StopReceiving();
            _disposed = true;
        }
    }
}

EventHandler.cs

namespace BotLesson
{
    internal class EventHandler : IEventHandler
    {
        public void OnCallbackQuery(object? sender, CallbackQueryEventArgs e)
        {
            Log.Debug("CallbackQueryEventArgs: {e}", e);
        }

        public void OnInlineQuery(object? sender, InlineQueryEventArgs e)
        {
            Log.Debug("InlineQueryEventArgs: {e}", e);
        }

        public void OnInlineResultChosen(object? sender, ChosenInlineResultEventArgs e)
        {
            Log.Debug("ChosenInlineResultEventArgs: {e}", e);
        }

        public void OnMessage(object? sender, MessageEventArgs e)
        {
            Log.Debug("MessageEventArgs: {e}", e);
        }

        public void OnMessageEdited(object? sender, MessageEventArgs e)
        {
            Log.Debug("MessageEventArgs: {e}", e);
        }

        public void OnReceiveError(object? sender, ReceiveErrorEventArgs e)
        {
            Log.Error(e.ApiRequestException, e.ApiRequestException.Message);
        }

        public void OnReceiveGeneralError(object? sender, ReceiveGeneralErrorEventArgs e)
        {
            Log.Error(e.Exception, e.Exception.Message);
        }

        public void OnUpdate(object? sender, UpdateEventArgs e)
        {
            Log.Debug("UpdateEventArgs: {e}", e);
        }
    }
}

SignalHandler.cs

这与我的问题没有直接关系,但它在第三方库侦听消息时使应用程序处于等待模式。

namespace BotLesson
{
    internal class SignalHandler : ISignalHandler
    {
        private readonly ManualResetEvent _resetEvent = new ManualResetEvent(false);
        private readonly SetConsoleCtrlHandler? _setConsoleCtrlHandler;

        public SignalHandler()
        {
            if (!NativeLibrary.TryLoad("Kernel32", typeof(Library).Assembly, null, out var kernel)) return;
            if (NativeLibrary.TryGetExport(kernel, "SetConsoleCtrlHandler", out var intPtr))
                _setConsoleCtrlHandler = (SetConsoleCtrlHandler) Marshal.GetDelegateForFunctionPointer(intPtr,
                    typeof(SetConsoleCtrlHandler));
        }

        public void Set()
        {
            if (_setConsoleCtrlHandler == null) Task.Factory.StartNew(UnixSignalHandler);
            else _setConsoleCtrlHandler(WindowsSignalHandler, true);
        }

        public void Wait()
        {
            _resetEvent.WaitOne();
        }

        public void Exit()
        {
            _resetEvent.Set();
        }

        private void UnixSignalHandler()
        {
            UnixSignal[] signals =
            {
                new UnixSignal(Signum.SIGHUP),
                new UnixSignal(Signum.SIGINT),
                new UnixSignal(Signum.SIGQUIT),
                new UnixSignal(Signum.SIGABRT),
                new UnixSignal(Signum.SIGTERM)
            };

            UnixSignal.WaitAny(signals);
            Exit();
        }

        private bool WindowsSignalHandler(WindowsCtrlType signal)
        {
            switch (signal)
            {
                case WindowsCtrlType.CtrlCEvent:
                case WindowsCtrlType.CtrlBreakEvent:
                case WindowsCtrlType.CtrlCloseEvent:
                case WindowsCtrlType.CtrlLogoffEvent:
                case WindowsCtrlType.CtrlShutdownEvent:
                    Exit();
                    break;

                default:
                    throw new ArgumentOutOfRangeException(nameof(signal), signal, null);
            }

            return true;
        }

        private delegate bool SetConsoleCtrlHandler(SetConsoleCtrlEventHandler handlerRoutine, bool add);

        private delegate bool SetConsoleCtrlEventHandler(WindowsCtrlType sig);

        private enum WindowsCtrlType
        {
            CtrlCEvent = 0,
            CtrlBreakEvent = 1,
            CtrlCloseEvent = 2,
            CtrlLogoffEvent = 5,
            CtrlShutdownEvent = 6
        }
    }
}

我的原始观点基于我对 SimpleInject 所做的一些假设——或者更具体地说,我使用 SimpleInject 的方式。

应用程序停留在 运行ning,等待 SignalHandler._resetEvent。同时,消息通过 Bot.cs 构造函数上的任何处理程序传入。

所以我的 thought/theory 是 Main launches Bot.Process,它直接依赖于 ITelegramClient 和 IEventHandler。在我的代码中,没有释放这些资源的机制,我怀疑我假设 IoC 会施展魔法并释放资源。

但是,根据 Visual Studio 内存使用情况,向机器人发送消息会不断增加对象的数量。这也反映在实际进程内存中。

不过,在编辑此 post 以供批准时,我想我可能最终误解了 Visual Studio 的诊断工具。在 运行 时间 15 分钟后,应用程序的内存利用率似乎停留在 36 MB 左右。或者它只是一次增加得很少以至于很难看到。

比较我在 1 分钟和 17 分钟时拍摄的内存使用情况快照,上面的每个对象似乎都创建了 1 个。如果我正确地阅读了这篇文章,我想这证明了 IoC 没有创建新对象(或者在我有机会创建快照之前它们就被处理掉了。

您的答案的关键在于您在分析应用程序内存时的观察结果:“上面的每个对象似乎都创建了 1 个”。由于所有这些对象都存在于无限应用程序循环中,因此您不必担心它们的生命周期。
从您发布的代码来看,唯一动态创建但不会在 Bot 生命周期内累积的昂贵对象是异常对象(及其关联的调用堆栈),尤其是当异常被 try-catch.

假设您使用的“Simple Injector”库工作正常,没有理由怀疑生命周期管理是否像您一样正确实施。这意味着它仅取决于容器的配置方式。

现在您的所有实例都具有 Transient 生命周期,这是默认设置。请务必注意这一点,因为您似乎期待 Singleton 生命周期。
Transient 表示每个请求新实例Singleton 相对,每个请求返回相同的共享实例要求。要实现此行为,您必须使用定义的 Singleton 生命周期显式注册导出:

// Container.GetInstance<IBot>() will now always return the same instance
Container.Register<IBot, Bot>(Lifestyle.Singleton);

永远不要使用服务定位器,尤其是在使用依赖注入时,只是为了管理对象的生命周期。如您所见,IoC conatiner 旨在处理该问题。这是每个 IoC 库都实现的关键功能。服务定位器可以而且应该被适当的 DI 替换,例如,与其传递 IoC 容器,不如将抽象工厂作为依赖项注入。对服务定位器的直接依赖引入了不需要的紧耦合。编写测试用例时很难模拟对服务定位器的依赖。

考虑到内存泄漏,Bot 的当前实现也很危险,特别是在导出的 TelegramBotClient 实例是 SingletonEventHandler 生命周期短暂。
您将 EventHandler 挂接到 TelegramBotClient。当 Bot 的生命周期结束时,您仍然有 TelegramBotClient 使 EventHandler 保持活动状态,这会造成内存泄漏。此外,Bot 的每个新实例都会将新的事件处理程序附加到 TelegramBotClient,从而导致多次重复处理程序调用。

为了始终安全起见,您应该在处理事件或范围生命周期结束时立即取消订阅事件,例如在 Closed 事件处理程序或 Dispose 方法中。在这种情况下,请确保对象由客户端代码正确处理。由于您不能始终保证 Bot 之类的类型得到正确处理,因此您应该考虑使用抽象工厂创建 TelegramBotClientEventHandler 的已配置共享实例。这个工厂 returns 一个共享的 TelegramBotClient 其中它的所有事件都被共享的 EventHandler.
观察到 这确保事件只订阅一次。

但最好的解决方案是使用 Weak-Event pattern.
您应该注意到这一点,因为您似乎很难确定对象的生命周期和潜在的内存泄漏。 使用您的代码很容易意外造成内存泄漏。

如果您想编写健壮的应用程序,了解造成内存泄漏的主要陷阱是必不可少的:Fighting Common WPF Memory Leaks with dotMemory, 8 Ways You can Cause Memory Leaks in .NET, 5 Techniques to avoid Memory Leaks by Events in C# .NET you should know