C# 长寿命连接多路复用器日志记录

C# 长寿命连接多路复用器日志记录,c#,memorystream,stackexchange.redis,C#,Memorystream,Stackexchange.redis,我用StackExchange.Redis为Redis编写了一个监控服务,并且订阅了某些事件。我面临的问题是日志记录。它需要一个文本编写器。理想情况下,我希望将其分流到事件日志,因此我使用MemoryStream支持的StreamWriter,并在基于任务的计时器上使用StreamReader转储到事件日志 此实现的问题是,在我的测试中,MemoryStream泄漏严重,即使我在每次读取后使用MemoryStream.SetLengthint进行清除。ConnectionMultipler.Co

我用StackExchange.Redis为Redis编写了一个监控服务,并且订阅了某些事件。我面临的问题是日志记录。它需要一个文本编写器。理想情况下,我希望将其分流到事件日志,因此我使用MemoryStream支持的StreamWriter,并在基于任务的计时器上使用StreamReader转储到事件日志

此实现的问题是,在我的测试中,MemoryStream泄漏严重,即使我在每次读取后使用MemoryStream.SetLengthint进行清除。ConnectionMultipler.Connect方法只接受一个对象,我看不到如何替换该对象,这意味着我也必须定期更新ConnectionMultiplexer

这听起来有问题吗?我错过什么了吗?更简单的方法似乎只管理一个对象,但我不知道如何控制它。下面是一个示例控制台应用程序来演示

class Program
{
    private static MemoryStream _loggingStream;
    private static StreamReader _reader;

    private static object _padlock = new object();

    static async Task Main(string[] args)
    {
        _loggingStream = new MemoryStream();
        _reader = new StreamReader(_loggingStream);

        var logWriter = new StreamWriter(_loggingStream);

        ThreadPool.QueueUserWorkItem(async state => await WriteLog());

        while (true)
        {
            Monitor.Enter(_padlock);

            try
            {
                await logWriter.WriteLineAsync("hello world " + DateTime.Now.ToLongTimeString());
                await logWriter.FlushAsync();
            }
            finally
            {
                Monitor.Exit(_padlock);
            }
        }
    }

    private static async Task WriteLog()
    {
        while (_loggingStream.Length == 0)
        {
            await Task.Delay(TimeSpan.FromMilliseconds(5));
        }

        string log;

        lock (_padlock)
        {
            _loggingStream.Position = 0;

            log = _reader.ReadToEnd();
            _reader.DiscardBufferedData();
            _loggingStream.SetLength(0);
        }

        Console.WriteLine(log);

        ThreadPool.QueueUserWorkItem(async state => await WriteLog());
    }
}

问题不在于内存流。问题在于Console.WriteLine。在典型的Windows配置中,向MemoryStream写入/从MemoryStream读取内容要比向控制台写入快得多。每次读取内存流时,您可能都会将其归零,但一旦清除了内存流,您就会放弃锁定,日志写入程序会很快开始旋转

在第一次迭代中,假设日志写入线程有5毫秒的时间来写入一些日志。将其写入控制台需要5毫秒以上的时间,因此,当控制台写入线程运行一次时,它拥有超过5毫秒的日志,这将比写入前5毫秒的日志所需的时间更长。。。因此,每当控制台写入线程完成对日志先前状态的写入时,它会发现它有更多的日志,并且写入日志的时间更长:是的,内存流消耗了所有内存,但这是因为它需要内存来存储所有日志,而控制台写入线程正忙于消耗最后一次加载

以下是一些数学,只是为了好玩:

d is rate at which logs are produced
c is how long it takes to consume a unit of logs
x(i) is the volume of logs produced during iteration i of the log-consumer
y(i) is how long it takes to consume the logs produced in iteration i
我们可以写出一些简单的方程:

y(i) = c*x(i)     (time to consume logs is a linear function of volume)
x(i+1) = d*y(i)   (volume is a linear function of time between iterations)
因此,我们可以确定与内存使用量成比例的日志量如何随每次迭代而变化

x(i+1) = d*c*x(i)
如果d*c>1,那么x呈指数增长:对内存使用不利,尽管它仍然只能在时间上线性增长,因为d是限制因素,我们关注的是每次迭代的成本,而不是时间

如果考虑1/C的日志消耗率,则很明显在

时满足了该条件。
d > 1/c (i.e. rate at which logs are produced is greater than the rate at which logs are consumed)
写入内存流比写入控制台便宜:d>1/c,因此我们有一个根本性的问题,再聪明也解决不了:您无法将如此大量的日志写入控制台

您可以在输出中看到这个问题,因为时间戳不能跟踪时钟时间:它会立即落后。删除Console.WriteLine会使应用程序在我的计算机上的容量保持在10MB左右。您还可以看到内存使用中的问题:有时它会跳转,这是控制台编写器启动新迭代、将整个流字节[]复制到char[]ReadToEnd并最终生成字符串的罕见事件:字节[]可以立即释放,这并不重要,因为有两个大小相同的对象来填充松弛

顺便说一句,使用SetLength0只会通过创建更多字节数组来掩盖问题,并且可能实际上增加了峰值内存使用率,因为它不会降低内存流的最大容量,并且意味着有丢弃的对象等待垃圾回收


正如评论中所讨论的,您不应该在线程之间访问监视器;使用wait意味着当控件返回到日志写入方法时,上下文将被保留,但不能保证您将获得相同的线程。

您正在跨异步调用进行日志记录-不能保证您的锁在被锁定的线程上被释放。您的显示器不安全,可能是许多问题的根源。我猜你忽略了编译器警告错误?您不能在异步调用周围使用锁,并使用监视器来绕过它。在这种情况下,您可以尝试使用SemaphoreSlim。@xxbbcc更正。我一直认为编译器是如何处理锁的,这让使用async/await很痛苦,并不是说监视器不好。@Bercovicadrian我已经进行了重构以使用SemaphoreSlim,但不幸的是,这并不能解决我的实际问题。不过,如果线程更安全的话,还是要谢谢你。@Bigsby lock被编译成监视器-它们是一个整体。大多数问题都是由于任务完成时在错误的线程上使用了锁造成的。您需要改变这一部分——SemaphoreSlim是一个很好的替代方案,但请尝试看看您是否可以在不锁定的情况下完成这项工作。可能不可能。我完全没有想到这一点。我确实试着设置了一些等待时间,因为我很好奇这是否是一个错误
排水率的问题,但我仍然看到上升,尽管速度较慢。不过,如果Console.WriteLine是一个问题,那么随着时间的推移,它的内存使用量会增加。@Bigsby如果您注释掉Console.WriteLine,它的内存使用量是否仍会增加到千兆字节?如果是这样的话,还有更多的东西在起作用,但在我的机器Win7,Framework4.7上,它肯定是这样的。这将有助于你们中的许多人提出证据,说明为什么你们怀疑记忆流;我从一开始就做了这项调查,测试了各种各样的东西,直到我发现Console.WriteLine在锁外面。正如前面的评论中所述,暗示存在泄漏是不够的:人们总是将正常GC行为误认为Win 10、4.7.2上的泄漏,我在25 MB左右徘徊,但我确实在徘徊。是的,那是我的泄密问题。我之所以认为这是内存流的泄漏或某些内在原因,是因为内存快照工具。通常情况下,当一个孤立的增量持续上升,并且在其自己的帐户上占用了99%以上的内存时,就是内存没有得到正确释放,因此出现了泄漏。这不是一个漏洞,更像是龟兔之间的资源利用竞赛。正如我所提到的,我已经对此进行了轻微的诊断,但没有测试控制台。WriteLine因为这对我来说从来都不是问题。@Bigsby我必须道歉,我完全误解了你之前的评论。很高兴你把它整理好了。