【问题标题】:FIFO Queue is causing IIS processor to max out at 100%FIFO 队列导致 IIS 处理器达到 100% 的最大值
【发布时间】:2013-01-10 15:00:44
【问题描述】:

我在 IIS 上有一个单例服务类作为 Web 应用程序的一部分(出于数据缓存原因,该服务是单例)。向服务发出请求的浏览器客户端可能会产生以下三种结果之一:

1) 缓存中有数据且数据未过期(陈旧) - 我们返回此数据。非常快。 2)缓存的数据过期了,但是另一个请求已经在查询数据库了。我们返回了缓存的数据。 3)缓存数据过期,没有请求对数据库进行查询。该请求向前移动以进行查询。

但是,针对同名存储过程的数据库查询必须排队(要求)。

因此,我编写了这个队列类,旨在将这些查询排队并连续运行它们,而不是同时运行。这些队列类根据需要创建并存储在单例类的列表中。当请求移动到第 (3) 部分时,它会找到与其存储过程名称匹配的队列类,并将请求提交给队列类。然后它一直等到数据从数据库返回,以便它可以为 HTML 请求提供服务。

不幸的是,在使用此代码几个小时后,服务器进程的最大值为 100%。

我不确定改进它的最佳方法是什么,因为多线程编码不是我的专长。

队列类代码如下:

public ReportTable GetReportTable(ReportQuery query)
{
  lock (_queue)
  {
    _queue.Enqueue(query);
    Monitor.Pulse(_queue);
  }

  lock (_queue)
  {
    var firstQueryInQueue = _queue.Peek();
    while (_inUse || firstQueryInQueue == null || firstQueryInQueue.GetHashCode() != query.GetHashCode())
    {
      Monitor.Pulse(_queue);
      Monitor.Wait(_queue);
    }

    _inUse = true;
    firstQueryInQueue = _queue.Dequeue();
    var table = firstQueryInQueue.GetNewReportTable();
    _inUse = false;

    Monitor.Pulse(_queue);
    return table;
  }
}

【问题讨论】:

  • 从 .NET 4.0 开始,框架中已经有一个 ConcurrentQueue 为你写了:( MSDN ) 你能用吗?
  • 我曾一度尝试使用 Nicholas,但它似乎并不真正适合我的情况。排队物品很容易。这是让调用线程等待而不会占用处理器时间的问题。
  • 如您所见,编写并发队列很难做到正确。如果可以的话,我建议你尝试使用框架实现。如果您想了解更多关于正确使用Monitor 的信息,我写了一篇您可能喜欢阅读的文章:Wait and Pulse demystified
  • @ChrisHolmes - 好的,改用 BlockingCollection。
  • 我不认为 BlockingCollection 解决了我的问题 Martin。也许我误解了我读过的文档。但在我的特殊情况下,调用此方法的线程与必须等待和消耗的线程相同。调用此方法的单个线程必须 (1) 将其项入队 (2) 等待其项找到到达队列前面的方式 (3) 将其项出列并开始工作。我需要做的是让每个调用线程等待而不占用处理器时间。我在这里尝试做的是接受并发调用并使它们串行,而不需要猛击处理器。

标签: c# multithreading iis queue fifo


【解决方案1】:

我不知道我是否理解问题 但是你可以很简单地重写它

private object _lockObj=new object();
public ReportTable GetReportTable(ReportQuery query)
{
  lock(_lockObj){
    var table = query.GetNewReportTable();
    return table;
  }
}

【讨论】:

  • 值得注意的是,关于你的回答:当你只依赖锁时,我的理解是它会导致当前线程旋转等待一段时间,然后最终被推入等待状态。在 spinwait 期间,它正在咀嚼 cpu 周期。我需要最有效的解决方案。我的第一个解决方案实际上与您的类似,并且在此应用程序运行几分钟后导致 CPU 达到 100% 的最大值。这就是我试图通过使用稍微复杂的解决方案来避免的。
  • 但是你已经使用了锁。考虑两个请求到达你的第二个锁的情况,然后其中一个锁住我的代码是如何工作的
【解决方案2】:

这就是我修复它的方法。

public ReportTable GetReportTable(ReportQuery query)
{
  lock (_queue)
  {
    _queue.Enqueue(query);
    Monitor.Pulse(_queue);
  }

  lock (_queue)
  {
    var firstQueryInQueue = _queue.Peek();
    while (_inUse || firstQueryInQueue == null || firstQueryInQueue.GetHashCode() != query.GetHashCode())
    {
      Monitor.Wait(_queue);
    }

    _inUse = true;
    firstQueryInQueue = _queue.Dequeue();
    var table = firstQueryInQueue.GetNewReportTable();
    _inUse = false;

    Monitor.Pulse(_queue);
    return table;
  }
}

它之前不起作用的原因是因为我对 Monitor.Wait() 和 Monitor.Pulse() 缺乏全面的了解。我在代码中的错误位置使用了 Pulse()。幸运的是,有一个很好的答案 here 很好地描述了 Wait() 和 Pulse()。

关键是在collection改变之后Pulse(),给后续排队的线程一个机会来测试条件,即:我的查询是不是队列中的第一个?其他人是否已经在进行查询?如果测试失败,线程会调用 Wait(),将其放入等待队列,并且不会占用处理器周期。当 在前面并正在执行查询的线程完成时,它将 _inUse 标志翻转为 false 并调用 Pulse(),唤醒下一个线程,以便它可以检查条件。

在实施了这个解决方案并观察了一天的任务管理器后,我很高兴看到服务器上的负载在几个小时内保持在 1% 到 5% 之间,而且 CPU 从未像以前那样爬升到 100%。

我对此进行了大量阅读,似乎 PulseAll() 在这种情况下可能是更好的调用,但到目前为止 Pulse() 正在工作,我们还没有遇到任何问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-10-12
    • 1970-01-01
    • 1970-01-01
    • 2015-01-26
    • 2014-09-04
    • 1970-01-01
    • 2011-08-19
    相关资源
    最近更新 更多