我有一个监视服务,我正在使用StackExchange.Redis
为Redis编写并订阅某些事件。我面临的问题是日志记录。它需要一个TextWriter
。理想情况下,我希望将其分流到EventLog
,所以我使用MemoryStream
支持的StreamWriter
,并在任务上使用EventLog
转储到StreamReader
基于计时器。
此实现的问题是,在我的测试中,MemoryStream
泄漏严重,即使每次读取后我都使用MemoryStream.SetLength(int)
进行清除。 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());
}
}
答案 0 :(得分:1)
问题不在于内存流。问题是Console.WriteLine
。向MemoryStream
中写入内容/从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
中读取内容要比例如在控制台中写入内容要快得多。典型的Windows配置。您可能会在每次读取时将内存流清零,但是一旦将其清除,便放弃了锁,日志记录器开始非常快速地旋转。
在第一次迭代中,假设日志写入线程有5ms的时间来写入一些日志。将其写入控制台所需的时间少于5ms,因此,当控制台编写线程运行一次之后,其日志记录的时间超过5ms,这将比写入前5ms的日志所花费的时间更长。因此,每次控制台编写线程完成写出日志的先前状态时,都会发现它具有更多的功能,并且需要花费甚至更长的时间来写出来:是的,内存流正在消耗所有内存,但这是因为在控制台编写线程正在忙于消耗内存的同时,它需要内存来存储所有日志。最后一次加载。
这里是一些数学,只是为了好玩:
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
,所以我们有一个根本的问题,聪明的问题无法解决:您不能写这样的东西到控制台的大量日志。
您可以在输出中看到这个问题,因为时间戳不能跟踪时钟时间:它会立即落后。删除byte[]
会使应用程序驻留在我的计算机上大约10MB。您还可以在内存使用量中看到问题:它不时地跳跃,这是控制台编写器开始新迭代并将整个流(char[]
)复制到{{ 1}}(ReadToEnd
)并最终产生一个string
:可以立即释放byte[]
并不重要,因为您有2个大小相同的对象可以容纳松弛
顺便说一句,使用SetLength(0)
仅会通过创建更多的字节数组来掩盖此问题,并且可能实际上增加了峰值内存使用量,因为这不会降低内存流的最大容量,并且周围有丢弃的物体等待被垃圾收集。
正如注释中所讨论的,您不应在线程之间访问监视器。您使用await
意味着在将控制权返回到日志写入方法时将保留上下文,但是不能保证您将获得相同的线程。