C# 长寿命连接多路复用器日志记录
我用StackExchange.Redis为Redis编写了一个监控服务,并且订阅了某些事件。我面临的问题是日志记录。它需要一个文本编写器。理想情况下,我希望将其分流到事件日志,因此我使用MemoryStream支持的StreamWriter,并在基于任务的计时器上使用StreamReader转储到事件日志 此实现的问题是,在我的测试中,MemoryStream泄漏严重,即使我在每次读取后使用MemoryStream.SetLengthint进行清除。ConnectionMultipler.Connect方法只接受一个对象,我看不到如何替换该对象,这意味着我也必须定期更新ConnectionMultiplexer 这听起来有问题吗?我错过什么了吗?更简单的方法似乎只管理一个对象,但我不知道如何控制它。下面是一个示例控制台应用程序来演示C# 长寿命连接多路复用器日志记录,c#,memorystream,stackexchange.redis,C#,Memorystream,Stackexchange.redis,我用StackExchange.Redis为Redis编写了一个监控服务,并且订阅了某些事件。我面临的问题是日志记录。它需要一个文本编写器。理想情况下,我希望将其分流到事件日志,因此我使用MemoryStream支持的StreamWriter,并在基于任务的计时器上使用StreamReader转储到事件日志 此实现的问题是,在我的测试中,MemoryStream泄漏严重,即使我在每次读取后使用MemoryStream.SetLengthint进行清除。ConnectionMultipler.Co
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我必须道歉,我完全误解了你之前的评论。很高兴你把它整理好了。