具有两个线程的STL队列(生产者,消费者)

时间:2013-01-28 13:19:00

标签: c++ multithreading winapi thread-safety

我想要队列安全的关键部分,以便线程不会同时访问队列。即使我评论与“关键”部分相关的行,此代码仍然有效。 任何人都可以解释原因吗?

queue<int> que;
CRITICAL_SECTION csection;
int i=0;

DWORD WINAPI ProducerThread(void*)
{

    while(1)
    {
        //if(TryEnterCriticalSection(&csection))
        {
            cout<<"Pushing value "<<i<<endl;
            que.push(i++);
            //LeaveCriticalSection(&csection);
        }
    }
}

//Consumer tHread that pops out the elements from front of queue
DWORD WINAPI ConsumerThread(void*)
{
    while(1)
    {
        //if(TryEnterCriticalSection(&csection))
        {
            if(!que.empty())
            {
                cout<<"Value in queue is "<<que.front()<<endl;
                que.pop();
            }
            else
                Sleep(2000);
            //LeaveCriticalSection(&csection);
        }
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE handle[2];
    //InitializeCriticalSection(&csection);
    handle[0]=NULL;
    handle[1]=NULL;
    handle[0]=CreateThread(0,0,(LPTHREAD_START_ROUTINE)ProducerThread,0,0,0);
    if(handle[0]==NULL)
        ExitProcess(1);

    handle[1]=CreateThread(0,0,(LPTHREAD_START_ROUTINE)ConsumerThread,0,0,0);
    if(handle[1]==NULL)
        ExitProcess(1);

    WaitForMultipleObjects(2,handle,true,INFINITE);

    return 0;
}

4 个答案:

答案 0 :(得分:2)

在您的特定情况下,cout将比“get”长几百倍。并且当队列为空时你睡觉,这允许另一个线程在你的“消费者”线程获取任何队列之前填满很多队列。

全速运行(无调试打印,无睡眠),确保运行一段时间,并使用简单的数学运算检查另一端的值。

这样的事情:

int old_val = val;
while(1)
{
    if(!que.empty())
    {
       int  val = que.front();

       que.pop();
       if (old_val+1 != val)
       {
          /// Do something as things have gone wrong!
       }
     }
}

请注意,这也可能不会立即/琐碎地出错。你想运行它几个小时,最好还是在机器上运行其他东西 - 比如批处理文件:

@echo off
:again 
dir c:\ /s > NUL:
goto again

[自从我为Windows编写批处理脚本以来已经有一段时间了,所以这可能不是100%正确,但我认为你应该能够对我做错的任何事情进行谷歌搜索 - 这个想法是“闯入” “机器]。

此外,尝试使用每对单独的队列运行您的线程对的多个副本 - 这将强制执行更多计划活动,并可能触发问题。

像安东说的那样,其中一些事情往往很难再现。我在实时操作系统中遇到了问题,其中队列搞砸了 - 唯一的真实迹象是内存最终在“压力测试”期间耗尽(“随机”事情,包括几个不同的中断源)。该操作系统已经在生产测试中的数百个单元中进行了测试,并作为真正的生产系统在现场运行[并且在不同处理器上运行的相同代码在全世界运行电话交换机,同样没有客户对内存泄漏的抱怨] ,看似没有内存泄漏!但是在一个函数中,队列处理中的一个“漏洞”只能运行。在认为是压力测试本身偶尔会遇到队列建立的奇怪情况之后,我最终发现了实际问题 - 队列的读写之间的中断 - 正好是两个教师的漏洞,并且只有在中断例程在发送消息时被另一个中断例程中断...我宁愿不再调试那个!

答案 1 :(得分:2)

意外,大概有两个原因:

  1. 它不起作用,但你从来没有注意到。消费者会拉出队列中的任何内容,或队列中的任何内容。如果没有任何东西,它会一直睡到生产者推了一些东西。这“有效”,因为生产者只追加到最后,而消费者只从头开始阅读。除了更新size。您很可能最终会有一个队列,该队列处于有元素的状态,但size没有反映它。这是令人讨厌的,但相反的,迟早也可能发生,甚至更加令人讨厌 你无从知晓。好吧,你最终可能知道,如果排队的工作项目由于某种原因“消失”或者你的内存不足,那么试着找出原因。
  2. 您使用printf(或std::cout,它是相同的),由关键部分内部锁定。这种“类型”以您需要的方式锁定对队列的访问,除非它不。它将在99.9%的时间内工作(不小心,因为消费者将被阻止尝试打印,这比生成器附加到队列所需的时间更长)。但是,当打印后刚刚发生上下文切换时,它突然失败。砰,你死了。
  3. 您确实绝对需要使用关键部分对象或互斥锁保护关键代码部分。否则,结果是不可预测的。与人们可能相信的相反,“但它有效”并不是一件好事,它是可能发生的最糟糕的事情。因为它只有在它没有的情况下才有效,然后你不知道为什么。

    也就是说,您可以使用IO完成端口,它可以非常高效地完成所有工作。您可以使用GetQueuedCompletionStatus从端口中提取“事件”,然后使用PostQueuedCompletionStatus发布一个。完成端口完成队列的整个处理,包括为您准备好与多个消费者的同步(并且它以LIFO顺序执行,这有利于避免上下文切换和高速缓存失效)。 每个事件都包含一个指向OVERLAPPED结构的指针,但是完成端口不使用它,你只需传递任何指针(或者,如果你感觉更好,那么传递指针到OVERLAPPED后面跟着你自己的数据。)

答案 2 :(得分:0)

这种错误CRITICAL_SECTION阻止的最大问题之一就是它很难重现它们。你必须预测它如何在没有能够证明它的情况下失败。

当您保护自己的代码而不是包装非线程安全的库调用时,通常可以通过向某个地方添加Sleep来触发竞争条件。在您发布的代码中,没有机会为生产者执行此操作(无论不变量是否已被破坏,它是否在que.push内完成),以及消费者检查的潜在TOCTTOU问题当只有一个使用者时,空队列不存在。如果我们可以将Sleep添加到队列实现中,那么我们就能够以可预测的方式出错。

答案 3 :(得分:-1)

如果只有一个生产者和一个消费者,则队列代码可能对此类弹出轮询是安全的。如果生产者push使用临时索引/指针将数据插入下一个空队列位置,并且只将增加的'temp index'存储到队列'next empty'成员中,则queue.empty可以返回true,直到它安全为止消费者要弹出的数据。此类操作可能是偶然设计的,也可能是偶然产生的。

一旦你有多个生产者或一个以上的消费者,它肯定会迟早爆炸。

编辑 - 即使队列对一个生产者和一个消费者来说是安全的,除非记录在案,否则你不应该依赖它 - 一些b * * * d将改变实施下一个版本:(