1

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

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

这是我的第一个基础控制台应用程序,一个机器人,它登录到服务提供商并等待消息。我来自 .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();
        }
    }
}

机器人.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;
        }
    }
}

事件处理程序.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);
        }
    }
}

信号处理器.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。在我的代码中,没有一种机制可以让这些资源消失,我怀疑我假设 IoC 将执行魔法并释放资源。

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

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

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

4

1 回答 1

1

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

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

现在你所有的实例都有一个瞬态生命周期,这是默认的。注意到这一点很重要,因为您似乎期待一个Singleton生命周期。
瞬态意味着每个请求都有一个新实例,Singleton则为每个请求返回相同的共享实例。要实现此行为,您必须使用定义的Singleton生命周期显式注册导出:

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

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

Bot在考虑内存泄漏时,当前的实现也是非常危险的,尤其是在导出的TelegramBotClient实例是Singleton并且EventHandler具有瞬态生命周期的情况下。
你钩EventHandlerTelegramBotClient. 当生命周期Bot结束时,您仍然TelegramBotClient保持EventHandler活动状态,这会造成内存泄漏。此外,每个新实例Bot都会将新的事件处理程序附加到TelegramBotClient,从而导致多个重复的处理程序调用。

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

但最可取的解决方案是使用弱事件模式
您应该注意到这一点,因为您似乎很难确定对象的生命周期和潜在的内存泄漏。使用您的代码很容易意外地造成内存泄漏。

如果您想编写健壮的应用程序,必须了解造成内存泄漏的主要陷阱:使用 dotMemory 解决常见的 WPF 内存泄漏、.NET 中导致内存泄漏的 8 种方法在 C# 中避免事件导致内存泄漏的 5 种技术.NET 你应该知道

于 2020-08-04T10:27:19.493 回答