3

我想要队列安全的关键部分,以便线程不会同时访问队列。即使我评论与关键部分相关的行,此代码也有效。谁能解释为什么?

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

4 回答 4

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% 正确,但我认为你应该能够在谷歌上搜索我出错的任何答案——这个想法是“中断”机器]。

此外,尝试运行一对线程的多个副本,每对有一个单独的队列——这将强制执行更多的调度活动,并可能引发问题。

就像安东说的那样,其中一些东西通常很难重现。我在实时操作系统中遇到了一个问题,队列被弄乱了——唯一真正的迹象是内存最终在“压力测试”期间耗尽(它会做“随机”的事情,包括几个不同的中断源)。该操作系统已经在数百个单元的生产测试中进行了测试,并作为真正的生产系统投入使用[并且在不同处理器上运行的相同代码再次在世界各地操作电话交换机,没有客户抱怨内存泄漏] ,貌似没有内存泄漏!但是队列处理中的一个“漏洞”,在一个函数中,只是运行。在认为是压力测试本身偶尔会遇到一些奇怪的情况导致排队时,

于 2013-01-28T13:31:44.250 回答
2

意外地起作用,大概有两个原因:

  1. 它不起作用,但你永远不会注意到。消费者拉出队列中的任何东西,或者它认为队列中的任何东西。如果什么都没有,它会一直休眠,直到生产者推送了一些东西。这“有效”是因为生产者只追加到末尾,而消费者只从头开始读取。除了更新size。您很可能最终会拥有一个处于存在元素但size不反映它的状态的队列。这是令人讨厌的,但相反的情况,也可能迟早发生,更令人讨厌。
    你没有办法知道。好吧,您最终可能会知道,排队的工作项是否由于某种原因“消失”或者内存不足,但请尝试找出原因。
  2. 您使用printf(或std::cout,相同),它在内部被关键部分锁定。这种“类型”以您需要的方式锁定对队列的访问,除非它没有。它将在 99.9% 的时间内工作(偶然地,因为消费者将被阻止尝试打印,这比追加到队列的生产者需要更长的时间来唤醒)。但是,当在打印之后发生上下文切换时,它会突然失败。砰,你死定了。

您确实绝对需要使用关键部分对象或互斥锁来保护关键代码部分。否则,结果是不可预测的。与人们可能相信的相反,“但它有效”不是一件好事,它是可能发生的最糟糕的事情。因为它只能在它不起作用之前起作用,然后你不知道为什么。

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

于 2013-01-28T13:33:48.763 回答
0

CRITICAL_SECTION防止这种错误的最大问题之一是很难重现它们。您必须预测它会如何失败而无法展示它。

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

于 2013-01-28T13:23:01.720 回答
-1

如果只有一个生产者和一个消费者,队列代码对于这种弹出轮询是安全的。如果生产者推送使用临时索引/指针将数据插入下一个空队列位置,并且仅将递增的“临时索引”存储到队列“下一个空”成员中,则 queue.empty 可以返回 true,直到它可以安全消费者要弹出的数据。这种操作可能是偶然设计的,也可能是偶然发生的。

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

编辑-即使一个生产者和一个消费者的队列证明是安全的,除非有文档记录,否则您不应依赖它-某些 b* * *d 将在下一个版本中更改实现:(

于 2013-01-28T16:49:25.327 回答