3

要求是:要处理的项目存储在一个全局队列中。几个处理线程从全局队列中获取项目进行处理。Producer线程连续快速将item添加到全局队列(比所有dealer线程的处理速度快得多。另外,handler线程是计算密集型的。最好的性能是完全使用CPU)。所以,我再使用一个countKeeping 线程来将队列的长度保持在特定范围内,就像BOTTOMTOP大致一样(只是为了防止内存使用过多)。

ManualResetEvent用来处理“可以添加到队列”状态更改。全局队列是

Queue<Object> mQueue = new Queue<Object>;
ManualResetEvent waitingKeeper = new ManualResetEvent(false);  

处理程序线程

void Handle()
{
    while(true)
    {
        Object item;
        lock(mQueue)
        {
            if(mQueue.Count > 0)
                item = mQueue.Dequeue();
        }
        // deal with item, compute-intensive
    }
}

生产者线程将调用 AddToQueue() 函数将项目添加到 mQueue。

void AddToQueue(Object item)
{
    waitingKeeper.WaitOne();
    lock(mQueue)
    {
        mQueue.Enqueue(item);
    }
}

countKeeping线程主要如下

void KeepQueueingCount()
{
    while(true)
    {
        // does not use 'lock(mQueue)'
        // because I don't need that specific count of the queue
        // I just need the queue does not cost too much memory
        if(mQueue.Count < BOTTOM)
            waitingKeeper.Set();
        else if(mQueue.Count > TOP)
            waitingKeeper.Reset();
        Thread.Sleep(1000);
    }
}

问题来了。
当我将 BOTTOM 和 TOP 设置为较小的数字时,例如 BOTTOM = 20,TOP = 100,它适用于四核 CPU(CPU 利用率高),但对于单 CPU 则效果不佳(CPU 利用率波动较大。 )。
当我将 BOTTOM 和 TOP 设置为更大的数字时,例如 BOTTOM = 100,TOP = 300,它适用于单 CPU,但不适用于四核 CPU。
两种环境,两种情况,内存都没有使用太多(最多50M左右)。

从逻辑上讲,更大的 BOTTOM 和 TOP 将有助于提高性能(当内存没有使用太多时),因为有更多的项目供处理程序线程处理。但事实似乎并非如此。

我尝试了几种方法来找到问题的原因。而且我刚刚发现,当我用于lock(mQueue)保持线程时,它在上述两种 CPU 条件下都能正常工作。
新的countKeeping线程主要是这样的

void KeepQueueingCount()
{
    bool canAdd = false;
    while(true)
    {
        lock(mQueue)
        {
            if(mQueue.Count < BOTTOM)
                canAdd = true;
            else if(mQueue.Count > TOP)
                canAdd = false;
        }
        if(canAdd)
            waitingKeeper.Set();
        else
            waitingKeeper.Reset();
        // I also did some change here
        // when the 'can add' status changed, will sleep longer
        // if not 'can add' status not changed, will sleep lesser
        // but this is not the main reason
        Thread.Sleep(1000);
    }
}

所以我的问题是

  1. 当我没有lockcountKeeping 线程中使用时,为什么全局队列的范围会在不同的 CPU 条件下影响性能(这里,性能主要是 CPU 利用率)?
  2. 当我lockcountKeeping thread中使用时,在不同的条件下性能都很好。有什么lock真正影响这一点?
  3. 有没有更好的方法来改变“可以添加”状态而不是使用 ManualResetEvent
  4. 有没有更好的模型适合我的要求?或者当生产者线程连续快速工作 时,有没有更好的方法来防止内存不被使用?

---更新---
生产者线程的主要部分如下。STEP是数据库中每个查询的项目数。依次查询,直到查询完所有项目。

void Produce()
{
    while(true)
    {
        // query STEP items from database
        itemList = QuerySTEPFromDB();
        if(itemList.Count == 0)
            // stop all handler thread
            // here, will wait for handler threads handle all items in queue
            // then, all handler threads exit
        else
            foreach(var item in itemList)
                AddToQueue(item);
    }
}
4

1 回答 1

4

您的并发队列示例是一个经典示例,说明原子比较和交换自旋锁往往会做得更好,因为争用非常高,但在锁中花费的时间很少(只是排队和出队的时间)。

https://msdn.microsoft.com/en-us/library/dd460716%28v=vs.110%29.aspx

还值得注意的是,.NET 已经为您提供了一个使用这种原子 CAS 自旋锁设计的并发队列。

如果您有一个竞争非常激烈的共享资源,并且只使用很短的时间,那么高级锁会变得非常昂贵。

如果我使用粗略的视觉类比(使用夸张的、人类水平的时间单位),想象你有一家商店,而且有一条线。但是店员的工作速度真的很快,每秒钟都在排队。

如果您在这里使用关键部分/互斥锁,就像每个客户在发现还没有轮到他们时打瞌睡并打盹一样。然后轮到他们时,必须有人叫醒他们:“嘿,现在轮到你了!醒醒!” - “什——嗯?哦,好吧。” 正如你所想象的,由于额外的时间阻塞/挂起线程,你也可以倾向于形成越来越大的线程等待轮到它们。

这也是您看到 CPU 利用率波动的原因。线程可能会在锁周围形成交通堵塞并暂停/进入睡眠状态,这会在它们睡眠和等待轮到它们时降低 CPU 利用率。这也是相当不确定的,因为多线程不一定以完美的预定义顺序执行代码,因此如果您的设计允许线程在锁周围形成交通堵塞,您会看到尖峰。您可能会在一个会话中走运并在这种时间敏感的情况下获得快速性能,然后在另一个会话中运气不佳并获得非常差的性能。在最坏的情况下,

如果您在这里使用低级自旋锁,就像客户发现排队时不会打瞌睡一样。他们只是站在那里非常不耐烦地等待,不断地寻找是否轮到他们。如果线路移动得非常快,那么这可以更好地工作。

你的问题有点不寻常,因为你的生产者生产的速度比消费者消费的快得多。一次可以生产多少的上限想法在这里似乎是明智的,但您可以通过这种方式限制处理。我也不确定你为什么在一个单独的计数保持线程中解决这个问题(不太了解那部分)。我认为您可以做到这一点,这样您的生产者就不会在达到某个上限时将项目排入队列,直到队列变小。

您可能希望保持该上限以避免垃圾邮件内存,但我认为您会做得更好(在使用适当的锁之后)睡眠或让生产者平衡处理分布,并在生产者入队时将其更多地偏向您的消费者要处理的项目。这样一来,当生产者达到该限制时,您最终不会干扰生产者——相反,重点是避免达到该限制,以便您的消费者有机会以不会明显落后于生产者的速度进行消费。

于 2015-05-19T04:35:39.050 回答