【问题标题】:Efficient consumer thread with multiple producers具有多个生产者的高效消费者线程
【发布时间】:2010-08-26 06:52:14
【问题描述】:

我正在尝试通过在必要时跳过昂贵的事件操作来提高生产者/消费者线程情况的效率,例如:

//cas(variable, compare, set) is atomic compare and swap
//queue is already lock free

running = false


// dd item to queue – producer thread(s)

if(cas(running, false, true))
{
  // We effectively obtained a lock on signalling the event
  add_to_queue()
  signal_event()
}
else
{
  // Most of the time if things are busy we should not be signalling the event
  add_to_queue()

  if(cas(running, false, true))
    signal_event()
}

...

// Process queue, single consumer thread

reset_event()

while(1)
{
  wait_for_auto_reset_event() // Preferably IOCP

  for(int i = 0; i &lt SpinCount; ++i)
    process_queue()

  cas(running, true, false)

  if(queue_not_empty())
    if(cas(running, false, true))
      signal_event()
}

显然试图让这些事情变得正确有点棘手(!)那么上面的伪代码是否正确?一个比实际需要的更多的事件信号的解决方案是可以的,但不是一个对每个项目都这样做的解决方案。

【问题讨论】:

  • 你的代码是什么语言的?
  • 是事件操作昂贵还是消费者线程处理也昂贵? (你也想避免这种情况?)
  • 对不起,我现在才注意到所有这些反馈!尽管我对在 CSharp 中复制类似内容感兴趣,但它在 C++ 中。如果对队列中的每个项目都进行事件同步操作,则该操作是最昂贵的。

标签: multithreading thread-safety producer-consumer


【解决方案1】:

这属于称为“过早优化”的“停止胡闹,重新开始工作”的子类别。 :-)

如果“昂贵”的事件操作占用了很大一部分时间,那么您的设计是错误的,而不是使用生产者/消费者,您应该使用关键部分/互斥锁,并从调用线程中完成工作。

如果您真的担心,我建议您对您的应用程序进行概要分析。

更新:

正确答案:

制片人

ProducerAddToQueue(pQueue,pItem){

    EnterCriticalSection(pQueue->pCritSec)
        if(IsQueueEmpty(pQueue)){
            SignalEvent(pQueue->hEvent)
        }

        AddToQueue(pQueue, pItem)
    LeaveCriticalSection(pQueue->pCritSec)
}

消费者

nCheckQuitInterval = 100; // Every 100 ms consumer checks if it should quit.

ConsumerRun(pQueue)
{
    while(!ShouldQuit())
    {
        Item* pCurrentItem = NULL;
        EnterCriticalSection(pQueue-pCritSec);
            if(IsQueueEmpty(pQueue))
            {
                ResetEvent(pQueue->hEvent)
            }
            else
            {
                pCurrentItem = RemoveFromQueue(pQueue);
            }
        LeaveCriticalSection(pQueue->pCritSec);

        if(pCurrentItem){
            ProcessItem(pCurrentItem);
            pCurrentItem = NULL;
        }
        else
        {
            // Wait for items to be added.
            WaitForSingleObject(pQueue->hEvent, nCheckQuitInterval);
        }

    }
}

注意事项:

  • 该事件是手动重置事件。
  • 受临界区保护的操作很快。仅当队列转换为空状态/从空状态转换时,才会设置或重置该事件。必须在关键部分内设置/重置它以避免竞争条件。
  • 这意味着关键部分只保留很短的时间。所以争用很少见。
  • 关键部分不会阻塞,除非它们被争用。所以上下文切换将很少见。

假设:

  • 这是一个真正的问题,而不是家庭作业。
  • 生产者和消费者的大部分时间都花在做其他事情上,即为队列准备好物品,在将它们从队列中移除后进行处理。
  • 如果他们大部分时间都在执行实际的队列操作,那么您不应该使用队列。我希望这是显而易见的。

【讨论】:

  • 我很抱歉,但这是一个非常糟糕的答案,尽管我感谢你试图提供帮助。对于我正在使用的吞吐量而言,关键部分和互斥锁太慢了。其中很多来自可能发生的操作系统上下文切换。
  • @wb,您只是在重新发明关键部分。这正是关键部分旨在解决的问题,因此只需使用它们。 除非存在争用,否则它们不会阻塞
  • 明确地说,您建议的解决方案是使用内存屏障/互锁比较和交换操作,以避免必须发出信号或等待事件对象/信号量。这正是关键部分所做的,而您的示例正是他们这样做的原因。这就是为什么你的问题的答案是“使用关键部分”——不是因为你的想法有什么问题,而是因为它已经被发明出来了。
【解决方案2】:

经历了一堆案例,看不出有什么问题。但这有点复杂。我想也许你会遇到 queue_not_empty / add_to_queue 比赛的问题。但看起来两条路径中的后主导 CAS 都涵盖了这种情况。

CAS 很昂贵(不像信号那么昂贵)。如果您希望跳过信号很常见,我会将 CAS 编码如下:

bool cas(variable, old_val, new_val) {
   if (variable != old_val) return false
   asm cmpxchg
}

像这样的无锁结构是Jinx(我从事的产品)非常擅长测试的东西。因此,您可能希望使用 eval 许可证来测试无锁队列和信号优化逻辑。


编辑:也许你可以简化这个逻辑。

running = false 

// add item to queue – producer thread(s) 
add_to_queue()
if (cas(running, false, true)) {
   signal_event()
}

// Process queue, single consumer thread 

reset_event() 

while(1) 
{ 
    wait_for_auto_reset_event() // Preferably IOCP 

   for(int i = 0; i &lt SpinCount; ++i) 
       process_queue() 

   cas(running, true, false)  // this could just be a memory barriered store of false

   if(queue_not_empty()) 
      if(cas(running, false, true)) 
         signal_event() 
} 

现在 cas/信号总是彼此相邻的,它们可以移动到子程序中。

【讨论】:

  • 对不起,我错过了所有这些 cmets - 谢谢你的! - 我只是花时间消化你现在所说的话......
  • 在您评论的第一部分(当我开始记起我之前的思路时!) - 是的,我故意围绕这种方式构建它(我认为!)以处理竞争条件我记得是在像安东尼的回答这样的例子中引起的。我想我是为了减少邪恶——在某些情况下让工人循环可能比在最坏情况下需要的次数更多。我现在正在更多地消化你的建议以及你对我之前想知道的内存屏障存储的评论类似的东西,但决定先尝试让它与联锁的东西一起工作:)
【解决方案3】:

为什么不将bool 与该事件相关联?使用cas 将其设置为true,如果cas 成功则发出事件信号,因为事件必须已清除。然后服务员可以在等待之前清除标志

bool flag=false;

// producer
add_to_queue();
if(cas(flag,false,true))
{
    signal_event();
}

// consumer
while(true)
{
    while(queue_not_empty())
    {
        process_queue();
    }
    cas(flag,true,false); // clear the flag
    if(queue_is_empty())
        wait_for_auto_reset_event();
}

这样,您只在队列中没有元素时等待,并且您只为每批项目发出一次事件信号。

【讨论】:

  • 抱歉忽略我的最后一条评论,我点击返回太快了! - 我想我尝试过这样的事情,但发现在清除标志位和队列为空之间存在竞争条件。
【解决方案4】:

我相信,您想在这个问题中实现类似的目标:
WinForms Multithreading: Execute a GUI update only if the previous one has finished. 它专门针对 C# 和 Winforms,但该结构可能很适合您。

【讨论】:

    猜你喜欢
    • 2017-02-01
    • 1970-01-01
    • 2016-08-30
    • 1970-01-01
    • 2023-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多