如果队列未锁定,则多线程的性能与全局队列的长度有关

本文关键字:队列 全局 性能 锁定 多线程 如果 | 更新日期: 2023-09-27 17:49:52

要求是:要处理的项存储在全局队列中。多个处理程序线程从全局队列中获取要处理的项目。生产者线程连续地和快速地向全局队列添加项目(比所有经销商线程的处理速度快得多)。而且,处理程序线程是计算密集型的。最好的性能是CPU完全使用)。因此,我再使用一个countKeeping线程来将队列的长度保持在一个特定的范围内,就像BOTTOM大致到TOP一样(只是为了防止内存使用过多)。

我使用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()函数将item添加到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. 当我在countKeeping线程中使用lock时,性能两者都是在不同的条件下。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);
    }
}

如果队列未锁定,则多线程的性能与全局队列的长度有关

您的并发队列示例是一个典型的例子,其中原子比较和交换自旋锁往往做得更好,给定非常高的争用,但在锁上花费的时间很少(只是排队和退队的时间)。

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

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

如果您有一个只在很短的时间内使用的高度竞争的共享资源,那么高级锁将变得非常昂贵。

如果我使用一个粗略的视觉类比(夸张的,人类级别的时间单位),想象你有一家商店,那里有一条线。但是店员工作的速度很快,队伍每秒钟都在移动。

如果你在这里使用临界区/互斥锁,就像每个顾客在发现还没轮到他们的时候打瞌睡和小睡。然后轮到他们的时候,必须有人叫醒他们:"嘿,现在轮到你了!醒醒吧!" - "世界卫生大会——嗯?哦,好吧。你可以想象,由于额外的时间阻塞/暂停线程,你也可以倾向于形成越来越大的线程排队等待轮到它们。

这也是您看到CPU利用率波动的原因。线程可能会在锁周围形成交通阻塞,并被挂起/进入睡眠状态,这将减少线程在睡眠和等待轮到它们时的CPU利用率。这也是相当不确定的,因为多线程并不一定以完美的预定义顺序执行代码,所以如果您的设计允许线程在锁周围形成交通堵塞,您就可以看到峰值。在这种时间敏感的情况下,您可能会在一个会话中幸运地获得快速性能,然后在另一个会话中不幸地获得非常差的性能。在最坏的情况下,你可以让CPU利用率低于使用过多锁的单线程(曾经在一个代码库中,开发人员有把互斥锁放在所有东西周围的习惯,我经常在一个2核机器上的性能关键区域看到10%的CPU利用率——这是在一个遗留代码库中,开发人员试图将多线程作为事后的考虑,认为可以在任何地方都使用锁)。而不是为多线程设计代码)。

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

你们的问题有些不寻常,因为你们的生产者生产的速度比消费者消费的速度快得多。在这里,一次可以生产多少的上限似乎是合理的,但您可以通过这种方式限制处理。我也不知道为什么你在一个单独的计数管理员线程中解决这个问题(我不太理解那部分)。我认为你可以这样做,如果达到了某个上限,你的生产者就不会对项目进行排队,直到队列变小。

你可能想要保持这个上限以避免垃圾内存,但我认为你会做得更好(在使用适当的锁之后)睡眠或产生生产者来平衡处理分布,并在生产者排队处理项目时将其更多地向消费者倾斜。这样,当你的生产者达到这个极限时,你就不会陷入困境——相反,关键是要避免达到这个极限,这样你的消费者才有机会以不会明显落后于生产者的速度消费。