如果队列未锁定,多线程的性能与全局队列的长度有关?
multithreading's performance is related to global queue's length if queue is not locked?
要求是:要处理的项目存储在全局队列中。多个 处理程序线程 从全局队列中获取要处理的项目。 生产者线程向全局队列连续和快速添加项目(比所有经销商线程的处理速度快得多. 另外,handler 线程是计算密集型的。最好的性能是 CPU 完全使用)。所以,我又使用了一个 countKeeping thread 来将队列的长度保持在一个特定的范围内,就像 from BOTTOM to TOP 大概(只是为了不让内存占用太多)。
我用ManualResetEvent
来处理'can add to queue'状态变化。全局队列是
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);
}
}
count保持线程主要是喜欢下面
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);
}
}
所以我的问题是
- 当我在countKeeping线程中没有使用
lock
时,为什么范围
全局队列影响性能(这里主要是性能CPU
利用率)在不同的 CPU 条件下?
- 当我在countKeeping线程中使用
lock
时,性能是
以及在不同的条件下。 lock
到底有什么影响
这个?
- 是否有更好的方法来更改 'can add' 状态而不是使用
ManualResetEvent
?
- 有没有更好的模型符合我的要求?或者有没有更好的
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 自旋锁设计。
如果您有一个竞争非常激烈且仅使用很短时间的共享资源,则更高级别的锁会变得非常昂贵。
如果我使用粗略的视觉类比(使用夸张的、人类级别的时间单位),想象一下你有一家商店并且有一条线。但是店员们的工作真的很快,队伍每秒都在移动。
如果你在这里使用批评section/mutex,就好像每个顾客在发现还没轮到他们时打瞌睡并小睡一样。然后轮到他们时,必须有人叫醒他们: -"Hey you, it's your turn now! Wake up!" -"Wha--huh? Oh okay." 你可以想象,由于额外的时间 blocking/suspending 线程,您也可以倾向于形成越来越大的线程等待轮到它们。
这也是您看到 CPU 利用率波动的原因。线程可以在锁周围形成交通拥堵并使 suspended/put 进入休眠状态,这会降低 CPU 在它们休眠和等待轮到它们时的利用率。这也是相当不确定的,因为多线程不一定以完美的预定义顺序执行代码,因此如果您的设计允许线程在锁周围形成交通堵塞,您就会看到尖峰。您可能会在一个会话中幸运地在这种时间敏感的情况下获得快速性能,然后在另一个会话中运气不佳并获得非常糟糕的性能。在最坏的情况下,您实际上可以获得 CPU 利用率低于单线程和过多锁(曾经在代码库中工作,开发人员习惯于在所有内容周围放置互斥锁,我经常看到 10%CPU 在 2 核机器上的性能关键区域的利用率——这是在遗留代码库中,开发人员事后尝试更多地使用多线程,认为可以在各处散布锁,而不是为以下设计代码多线程)。
如果你在这里使用一个低级别的自旋锁,就像客户在发现有一条线时不会打瞌睡一样。他们只是站在那里非常不耐烦地等待,不断地看看是否轮到他们了。如果线路移动得非常快,那么这会更好。
你的问题有点不寻常,因为你的生产者生产的速度比消费者消耗的快得多。一次可以生产多少的上限想法在这里似乎是明智的,但您可以通过这种方式限制处理。我也不确定你为什么在一个单独的计数管理员线程中解决这个问题(不太理解那部分)。我认为你可以做到这一点,这样你的生产者就不会在达到某个上限时将项目排入队列,直到队列变小。
您可能希望保持该上限以避免内存垃圾,但我认为您最好(在使用适当的锁之后)休眠或让生产者平衡处理分配并使其更偏向您的每当生产者排队要处理的项目时,消费者。这样您就不会在生产者达到该限制时最终干扰他们——相反,重点是避免达到该限制,以便您的消费者有机会以不会明显落后于生产者的速度消费。
要求是:要处理的项目存储在全局队列中。多个 处理程序线程 从全局队列中获取要处理的项目。 生产者线程向全局队列连续和快速添加项目(比所有经销商线程的处理速度快得多. 另外,handler 线程是计算密集型的。最好的性能是 CPU 完全使用)。所以,我又使用了一个 countKeeping thread 来将队列的长度保持在一个特定的范围内,就像 from BOTTOM to TOP 大概(只是为了不让内存占用太多)。
我用ManualResetEvent
来处理'can add to queue'状态变化。全局队列是
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);
}
}
count保持线程主要是喜欢下面
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);
}
}
所以我的问题是
- 当我在countKeeping线程中没有使用
lock
时,为什么范围 全局队列影响性能(这里主要是性能CPU 利用率)在不同的 CPU 条件下? - 当我在countKeeping线程中使用
lock
时,性能是 以及在不同的条件下。lock
到底有什么影响 这个? - 是否有更好的方法来更改 'can add' 状态而不是使用
ManualResetEvent
? - 有没有更好的模型符合我的要求?或者有没有更好的 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 自旋锁设计。
如果您有一个竞争非常激烈且仅使用很短时间的共享资源,则更高级别的锁会变得非常昂贵。
如果我使用粗略的视觉类比(使用夸张的、人类级别的时间单位),想象一下你有一家商店并且有一条线。但是店员们的工作真的很快,队伍每秒都在移动。
如果你在这里使用批评section/mutex,就好像每个顾客在发现还没轮到他们时打瞌睡并小睡一样。然后轮到他们时,必须有人叫醒他们: -"Hey you, it's your turn now! Wake up!" -"Wha--huh? Oh okay." 你可以想象,由于额外的时间 blocking/suspending 线程,您也可以倾向于形成越来越大的线程等待轮到它们。
这也是您看到 CPU 利用率波动的原因。线程可以在锁周围形成交通拥堵并使 suspended/put 进入休眠状态,这会降低 CPU 在它们休眠和等待轮到它们时的利用率。这也是相当不确定的,因为多线程不一定以完美的预定义顺序执行代码,因此如果您的设计允许线程在锁周围形成交通堵塞,您就会看到尖峰。您可能会在一个会话中幸运地在这种时间敏感的情况下获得快速性能,然后在另一个会话中运气不佳并获得非常糟糕的性能。在最坏的情况下,您实际上可以获得 CPU 利用率低于单线程和过多锁(曾经在代码库中工作,开发人员习惯于在所有内容周围放置互斥锁,我经常看到 10%CPU 在 2 核机器上的性能关键区域的利用率——这是在遗留代码库中,开发人员事后尝试更多地使用多线程,认为可以在各处散布锁,而不是为以下设计代码多线程)。
如果你在这里使用一个低级别的自旋锁,就像客户在发现有一条线时不会打瞌睡一样。他们只是站在那里非常不耐烦地等待,不断地看看是否轮到他们了。如果线路移动得非常快,那么这会更好。
你的问题有点不寻常,因为你的生产者生产的速度比消费者消耗的快得多。一次可以生产多少的上限想法在这里似乎是明智的,但您可以通过这种方式限制处理。我也不确定你为什么在一个单独的计数管理员线程中解决这个问题(不太理解那部分)。我认为你可以做到这一点,这样你的生产者就不会在达到某个上限时将项目排入队列,直到队列变小。
您可能希望保持该上限以避免内存垃圾,但我认为您最好(在使用适当的锁之后)休眠或让生产者平衡处理分配并使其更偏向您的每当生产者排队要处理的项目时,消费者。这样您就不会在生产者达到该限制时最终干扰他们——相反,重点是避免达到该限制,以便您的消费者有机会以不会明显落后于生产者的速度消费。