我正在为我们正在运行的ASP.NET应用程序编写自己的记录器。使用当前的代码库,我们似乎有一个线程问题,我正在尝试逐个组件地消除这个问题的原因。启动应用程序后,网络服务器上的CPU达到100%。
我想要消除的第一件事是我写的记录器,因为这是代码库的最新增加之一。记录器背后的想法如下:
可以从任何地方调用静态类,XmlLogger有一个后台线程,它接收来自MessageQueue的消息并对其进行处理。所以我需要让这个队列的访问线程安全。 MessageQueue类的代码如下所示:
public sealed class MessageQueue
{
#region Private Fields
private readonly Queue<IMessage> _messageQueue;
#endregion
#region Constructor
/// <summary>
/// <para>Creates a new instance of the class and initializes all internal fields.</para>
/// </summary>
public MessageQueue()
{
_messageQueue = new Queue<IMessage>();
}
#endregion
#region Properties
/// <summary>
/// <para>Gets the number of <see cref="IMessage"/> objects in the queue.</para>
/// </summary>
public int NumberOfMessage
{
get
{
lock(_messageQueue)
{
return _messageQueue.Count;
}
}
}
#endregion
#region Public Members
/// <summary>
/// <para>Adds a new <see cref="IMessage"/> to the bottom of the queue.</para>
/// </summary>
/// <param name="message">The <see cref="IMessage"/> to be added to the queue.</param>
public void AddMessage(IMessage message)
{
lock(_messageQueue)
{
_messageQueue.Enqueue(message);
Monitor.PulseAll(_messageQueue);
}
}
/// <summary>
/// <para>Gets the first <see cref="IMessage"/> from the queue.</para>
/// </summary>
/// <returns>The first <see cref="IMessage"/> from the queue.</returns>
public IMessage GetMessage()
{
lock(_messageQueue)
{
while (_messageQueue.Count == 0) Monitor.Wait(_messageQueue);
return _messageQueue.Dequeue();
}
}
#endregion
}
将消息添加到任何记录器的MessageQueue的代码如下所示:
/// <summary>
/// <para>Logs a specified <see cref="IMessage"/> in the various loggers.</para>
/// </summary>
/// <param name="message">The <see cref="IMessage"/> to be logged.</param>
public void LogMessage(IMessage message)
{
lock(_loggerLock)
{
foreach (AbstractLogger logger in _loggers)
logger.MessageQueue.AddMessage(message);
}
}
我已经锁定了LogManager,因为消息的加载不应该与从队列中添加/删除记录器相冲突。我确定这里不存在问题。 下面的代码显示了XmlLogger中包含MessageQueue对象的处理例程。
private void ProcessMessage()
{
// This thread should be running indefinitly untill the abort is called.
while (true)
{
try
{
// Get the first message available from the Queue
IMessage message = MessageQueue.GetMessage();
// If we could retrieve a valid message, process it.
if (message != null && (LogSettings.Level & message.Level) == message.Level)
{
// Determine the path to the logfile
string logpath = DetermineFilePath(message.Context);
// Write the message into the file.
WriteMessage(logpath, message);
}
}
catch(ThreadAbortException)
{
break;
}
catch (Exception ex)
{
Debug.WriteLine(string.Format("InnerException caught: '{0}'", ex.Message));
}
finally
{
Thread.Sleep(100);
}
}
}
我想知道的是,这段代码是使用Monitor.Pulse&amp; Monitor.Wait例程?如果没有,我应该改变什么以防止出现问题?
编辑:请不要就使用xisting logger进行任何问题/讨论。这不是一个选择
EDIT2 :从anwser中取出工具,这是输出:
------------------------------------ 5956 Kernel:0 User:156250 TenForce.Execution。 Logging.Loggers.XmlLogger.ProcessMessage System.Threading.ExecutionContext.Run System.Threading.ExecutionContext.Run System.Threading.ThreadHelper.ThreadStart其他堆栈:
TenForce.Execution.Logging.Loggers.XmlLogger.ProcessMessage System.Threading.ExecutionContext.Run System.Threading.ExecutionContext.Run System.Threading.ThreadHelper.ThreadStart
对我来说听起来有点太高了......
答案 0 :(得分:3)
是的,看起来不错;你的dequeue正确地执行检查/循环(并且假设一个脉冲意味着现在有数据,这可能导致异常),所以大多数时间应该是空闲的。你的入队是相似的。也许我会补充:如果您添加了第一项,则只有脉冲:
lock(_messageQueue)
{
_messageQueue.Enqueue(message);
if(_messageQueue.Count == 1) Monitor.PulseAll(_messageQueue);
}
(否则,您的读者不会等待)
要研究高CPU,使用CPU分析器 - 猜测通常不是一个好方法。我们使用的Sam Saffron has one here;它可以在生产网络服务器上使用(只是......不是所有的时间!)
答案 1 :(得分:1)
我在你发布的代码中看不到任何奇怪的用法,你锁定了在构造函数中创建的只读对象,以及Wait()
在获取锁之后几乎立即退出锁的上下文的线程。
此外,卡在Wait()
中的线程不应导致处理器利用率上升。
当然,发现问题要比验证“一切正常”更容易 - 如果它很容易重现,你可以写一个空的日志实现,看看问题是否仍然存在,例如。
答案 2 :(得分:0)
您确定要编写自己的日志记录库吗?有几个成熟的开源记录器已经在生产中进行了详尽的测试,因此不太可能导致任何问题。这是对你上一个问题('我应该改变什么......')的回答,我推荐NLog。
PS解决关于我的回答与该主题无关的评论 以下是我在此代码中看到的问题
答案 3 :(得分:0)
我想知道的是,这段代码是正确使用的wya Monitor.Pulse&amp; Monitor.Wait例程?
是的,您的代码将有效。我一直在寻找的大红旗是在调用Monitor.Wait
后重新检查等待条件。如果你没有重新检查这个条件,那么另一个消费者可以刷掉最后一个项目,让当前消费者试图从空队列中取出。
如果没有,我应该改变什么以防止出现问题?
就像我说的,你的代码会起作用。但是,如果有多个消费者,那就不是最佳选择。 PulseAll
将等待队列中的每个线程转移到就绪队列中。因此,如果您只向队列添加一个项目,则所有线程都将被唤醒并竞争该单个项目。
生产者 - 消费者模式通常在Pulse
调用时效果更好。如果只有一个项目排队,那么只有一个线程被唤醒。如果多个项目快速连续排队,则会唤醒多个线程以处理队列。重要的是要注意使用Pulse
,假设每次都调用它,而不仅仅是Queue.Count == 1
对多个生产者和多个消费者来说仍然是安全的。
根据您发布的代码我还有其他不相关的评论。不要使用Thread.Abort
来终止线程。处理非常困难并且可能导致当前应用程序域中的数据损坏,因为ThreadAbortException
被注入线程执行序列中的不可预测的点。在您的情况下,更好的机制是将null
项排队作为终止工作线程的信号。您可以为每个工作线程排队一个null
项。