Почему использование памяти постоянно увеличивается при использовании внедрения зависимостей в консольном приложении C#? - PullRequest
0 голосов
/ 04 августа 2020

Возможно, я знаю ответ на свой заданный вопрос: я использую внедрение зависимостей конструктора во всем приложении, которое представляет собой зацикленное консольное приложение C#, которое не завершается после каждого запроса.

Я подозреваю, что Из-за этого время жизни всех включенных объектов практически бесконечно. При попытке настроить время жизни во время регистрации он предупреждает, что временный объект не может быть реализован на одноэлементном объекте из-за зависимостей (которые вдохновили взглянуть на использование памяти и этот вопрос).

Это мой первый опыт консольное приложение, бот, который входит в систему поставщика услуг и ожидает сообщений. Я пришел из. NET Core Web API, который снова имеет зависимости повсюду, но я думаю, что ключевое различие здесь ниже всего моего кода - это сама платформа, которая обрабатывает каждый запрос индивидуально, а затем убивает запущенный поток.

Насколько я близок? Должен ли я отделить самого бота от базового консольного приложения, которое слушает поставщика услуг, и пытаться реплицировать платформу, которую предоставляет маршрутизация IIS / kestrel / MVC для разделения отдельных запросов?

Edit : Изначально я задумал этот вопрос скорее как принцип дизайна, передовой опыт или вопрос направления. Люди запросили воспроизводимый код, поэтому здесь мы go:

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.

Приложение продолжает работать, ожидая SignalHandler._resetEvent. Между тем сообщения поступают через любой из обработчиков конструктора Bot.cs.

Итак, моя мысль / теория: Main запускает Bot.Process, который напрямую зависит от ITelegramClient и IEventHandler. В моем коде нет механизма, позволяющего использовать эти ресурсы go, и я подозреваю, что предполагал, что Io C собирался выполнить magi c и освободить ресурсы.

Однако отправка сообщений на бот постоянно увеличивает количество объектов в соответствии с использованием памяти Visual Studio. Это отражается и на реальной памяти процесса.

Хотя, редактируя этот пост для утверждения, я думаю, что в конечном итоге я мог неправильно интерпретировать инструменты диагностики c Visual Studio. Похоже, что использование памяти приложением составляет около 36 МБ после 15 минут работы. Или он просто увеличивается так мало за раз, что это трудно увидеть.

Сравнивая снимки использования памяти, которые я сделал за 1 минуту и ​​17 минут, похоже, что был создан по 1 из каждого из вышеперечисленных объектов. Если я правильно это читаю, я полагаю, что это доказывает, что Io C не создает новые объекты (или они удаляются до того, как у меня появится возможность создать снимок.

1 Ответ

1 голос
/ 04 августа 2020

Ключ к вашему ответу находится в резюме вашего наблюдения при профилировании памяти вашего приложения: «похоже, что был создан 1 из каждого из вышеперечисленных объектов» . Поскольку все эти объекты живут внутри бесконечного приложения l oop, вам не нужно беспокоиться об их времени жизни. Из опубликованного вами кода единственными дорогостоящими объектами, которые создаются динамически, но не накапливаются за время жизни 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);

Никогда не используйте локатор службы, особенно при использовании внедрения зависимостей, только для управления временем жизни объекта. Как видите, Conatiner Io C предназначен для этого. Это ключевая функция, которая реализована в каждой библиотеке Io C. Service Locator может и должен быть заменен правильным DI, например, вместо передачи контейнера Io C вы должны внедрять абстрактные фабрики в качестве зависимости. Прямая зависимость от Service Locator создает нежелательную тесную связь. Очень сложно имитировать зависимость от Service Locator при написании тестовых примеров.

Текущая реализация Bot также довольно опасна, если думать об утечках памяти, особенно в случае, если экспортированный экземпляр TelegramBotClient является Singleton и EventHandler с переходным временем жизни. Вы подключаете EventHandler к TelegramBotClient. Когда время жизни Bot заканчивается, у вас все еще есть TelegramBotClient, поддерживающий EventHandler живым, что создает утечку памяти. Кроме того, каждый новый экземпляр Bot будет присоединять новые обработчики событий к TelegramBotClient, что приводит к множеству дублирующих вызовов обработчиков.

Чтобы всегда быть в безопасности, вы должны либо немедленно отказаться от подписки на события, когда они обрабатывается, или когда время жизни области заканчивается, например, в обработчике событий Closed или в методе Dispose. В этом случае убедитесь, что объект правильно размещен клиентским кодом. Поскольку вы не всегда можете гарантировать, что такой тип, как Bot, будет правильно удален, вам следует подумать о создании настроенных общих экземпляров TelegramBotClient и EventHandler с использованием абстрактной фабрики. Эта фабрика возвращает общий TelegramBotClient, где все его события наблюдаются общим EventHandler. Это гарантирует, что события будут подписаны только один раз.

Но наиболее предпочтительным решением является использование шаблона Weak-Event . Вы должны заметить это, поскольку у вас, похоже, есть некоторые проблемы с определением времени жизни объектов и потенциальных утечек памяти. Используя ваш код, очень легко случайно создать утечку памяти.

Если вы хотите писать надежные приложения, важно знать основные подводные камни для создания утечек памяти: Борьба с распространенными утечками памяти WPF с помощью dotMemory , 8 способов вызвать утечку памяти в. NET, 5 способов избежать утечек памяти по событиям в C#. NET вы должны знать

...