正确使用Monitor.PulseAll?

时间:2011-11-29 07:36:30

标签: c# .net multithreading

我正在为我们正在运行的ASP.NET应用程序编写自己的记录器。使用当前的代码库,我们似乎有一个线程问题,我正在尝试逐个组件地消除这个问题的原因。启动应用程序后,网络服务器上的CPU达到100%。

我想要消除的第一件事是我写的记录器,因为这是代码库的最新增加之一。记录器背后的想法如下:

  • 1个名为LogManager的静态类,用于跟踪所有ILogger实现
  • 1个名为LogSettings的静态类,用于跟踪配置文件中的所有设置。
  • 1个XmlLogger,它有一个MessageQueue来保存消息并将它们写入xml文件。

可以从任何地方调用静态类,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         

对我来说听起来有点太高了......

4 个答案:

答案 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解决关于我的回答与该主题无关的评论 以下是我在此代码中看到的问题

  1. 每秒记录的消息不超过10条(thread.sleep)
  2. 独占锁定不允许从多个线程进行并行记录,因此实际上是序列化所有记录器调用
  3. 在每条日志消息上打开和关闭文件不是最快的解决方案 。
  4. 如果您使用多个日志记录线程(查看出列队列)
  5. ,则可能会出现问题

答案 3 :(得分:0)

  

我想知道的是,这段代码是正确使用的wya   Monitor.Pulse&amp; Monitor.Wait例程?

是的,您的代码将有效。我一直在寻找的大红旗是在调用Monitor.Wait后重新检查等待条件。如果你没有重新检查这个条件,那么另一个消费者可以刷掉最后一个项目,让当前消费者试图从空队列中取出。

  

如果没有,我应该改变什么以防止出现问题?

就像我说的,你的代码会起作用。但是,如果有多个消费者,那就不是最佳选择。 PulseAll将等待队列中的每个线程转移到就绪队列中。因此,如果您只向队列添加一个项目,则所有线程都将被唤醒并竞争该单个项目。

生产者 - 消费者模式通常在Pulse调用时效果更好。如果只有一个项目排队,那么只有一个线程被唤醒。如果多个项目快速连续排队,则会唤醒多个线程以处理队列。重要的是要注意使用Pulse,假设每次都调用它,而不仅仅是Queue.Count == 1对多个生产者和多个消费者来说仍然是安全的。

根据您发布的代码我还有其他不相关的评论。不要使用Thread.Abort来终止线程。处理非常困难并且可能导致当前应用程序域中的数据损坏,因为ThreadAbortException被注入线程执行序列中的不可预测的点。在您的情况下,更好的机制是将null项排队作为终止工作线程的信号。您可以为每个工作线程排队一个null项。