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

标签 c# multithreading performance locking manualresetevent

需求是:要处理的项目存放在一个全局队列中。多个处理程序线程从全局队列中获取要处理的项目。 生产者线程向全局队列连续快速添加项目(比所有经销商线程的处理速度快得多。此外,处理程序线程是计算密集型。最佳性能是完全使用 CPU)。因此,我又使用了一个countKeeping thread 来将队列的长度保持在特定范围内,就像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
    }
}

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

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

countKeeping thread主要是像下面这样

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. 当我在countKeeping线程中没有使用lock时,为什么范围 全局队列影响性能(这里主要是CPU性能 利用率)在不同的 CPU 条件下?
  2. 当我在 countKeeping thread 中使用 lock 时,性能是 以及在不同的条件下。 lock 的真正作用是什么 这个?
  3. 有没有更好的方法来更改“可以添加”状态而不是使用 ManualResetEvent?
  4. 是否有更好的模型符合我的要求?或者有没有更好的 producer thread 工作时避免内存使用过多的方法 持续快速

---更新---
Producer thread的主要部分如下。 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 自旋锁设计的并发队列。

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

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

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

这也是您看到 CPU 使用率波动的原因。线程可能会在锁周围形成交通拥堵并被挂起/进入休眠状态,这会降低它们在 sleep 和等待轮到它们时的 CPU 利用率。这也是相当不确定的,因为多线程不一定以完美的预定义顺序执行代码,因此如果您的设计允许线程在锁周围形成交通堵塞,您就会看到尖峰。您可能会在一个 session 中幸运地在这种时间敏感的情况下获得快速性能,然后在另一个 session 中运气不佳并获得非常糟糕的性能。在最坏的情况下,您实际上可以使 CPU 利用率低于具有过多锁的单线程(曾经在代码库中工作,开发人员习惯于在所有内容周围放置互斥锁,我经常看到 10% 的性能 CPU 利用率- 2 核机器上的关键区域——这是在遗留代码库中,开发人员事后尝试更多地使用多线程,认为可以在各处撒上锁,而不是为多线程设计代码。

如果您在这里使用低级自旋锁,就好像客户在发现排队时不会打瞌睡一样。他们只是站在那里非常不耐烦地等待,不断地看看是否轮到他们了。如果生产线移动得非常快,那么它的效果会好得多。

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

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

关于c# - 如果队列未锁定,多线程的性能与全局队列的长度有关?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30316555/

相关文章:

c# - 远程服务器返回错误 : 227 Entering Passive Mode (500 oops vs_utility_recv_peek: no data)

java - Servlet doGet 同步 - 不起作用?

乱序多线程处理和对话框的Android问题

multithreading - 在ZeroMQ中从多个线程到zmq_poll()一个REP套接字+ send()安全吗?

c# - ASP.NET 文件上传控件允许的最大文件大小

c# - Azure Blob 存储下载所有文件

c# - AutoMapper 自动创建 createMap

mysql - 为什么 ActiveRecord destroy_all 需要这么长时间?

使用 RouteBase 的 C# MVC 路由

java - 什么是好的 java 调试器?