【问题标题】:Could std::list be used for a simple lock-free queue?std::list 可以用于简单的无锁队列吗?
【发布时间】:2018-09-12 12:13:12
【问题描述】:

我在 C++11 中实现了一个多线程应用程序,它使用一个线程来轮询来自 QUdpSocket(QT5 框架)的数据,并使用另一个线程来处理收集的数据。

当套接字检测到传入数据时,会发出ReadyRead() 信号,所有数据报都会从QUdpSocket 中提取并复制到std::list 中。

void SocketWrapper::QueuePendingDatagrams()
{    
  while (hasPendingDatagrams()) {
    int dataSize = pendingDatagramSize();
    QByteArray tmpQByte = receiveDatagram().data();
    datagramList.push_back(tmpQByte); // see const_data
  }
  emit newDatagrams(&datagramList);
}

然后在另一个线程中启动消费者并检索列表的第一个元素,直到列表为空。

void Worker::ProcessDatagrams(std::list<QByteArray>* pDatagramList)
{
  while (pDatagramList->size() > 0) {
      QByteArray tmpDatagram = pDatagramList->front();
      pDatagramList->pop_front();

      // Process and log the data within the datagram...
}

我的问题是: 由于我总是弹出第一个元素并在最后一个元素之后推送,这种无锁方法是否线程安全

我可能(并且将会)在数据处理期间向套接字接收更多数据,因此两个线程最终将同时运行。对我来说,似乎唯一可能发生的数据竞争可能会高估DatagramList-&gt;size(),但我不明白为什么会出现问题。如果我不断收到数据,我相信无论如何都会消耗整个列表。

【问题讨论】:

  • 使用列表有什么特别的原因吗?对我来说看起来像是一个队列,在弹出之前移动数据通常是一件好事。
  • 在我看来,它不是重复的,因为在@Swift-FridayPie 的问题中,使用了互斥锁。如果我理解得很好,它会提供答案。列表不能直接由线程共享,因为它可能导致线程损坏。
  • @Bertinchamps 这不是完全相同的问题,但我们通常也使用重复项来表示“您的答案可能会在此处找到”。然而,在这种情况下,它是一种边界,所以我要离开它
  • @OZ17 上榜没有特别的原因。我对 C++ 很陌生,发现该列表可用于 FIFO 处理。我在这里检查了stackoverflow.com/questions/1262808/… 的区别,为了清楚起见,我确实应该使用这种结构。

标签: c++ multithreading list qudpsocket


【解决方案1】:

没有。

你在正确的路线上,但实际上你不能同时从不同的线程调用 any 容器成员函数并期望它可靠地工作,因为这些成员函数的实现它们本身不是 [保证是] 线程安全的。

我不明白为什么会有问题

私下里,我同意在 std::list 的特殊情况下,我不会对这里的实际问题抱太大期望。一些商店可能认为这是继续并在生产中使用它的充分理由。不过,我不会在代码审查中接受它。关键是我们根本无法*确切地知道内脏在做什么。

话虽这么说,如果您有一些特殊的 stdlib 实现(或带有特殊标志/开关/选项的 stdlib 实现)确实提供了这种保证,那么就将自己淘汰;)*最终您可以检查它的代码并为自己做出决定,但老实说,这就是疯狂所在!

【讨论】:

  • size 的 O(1) 复杂性要求意味着问题仍然可能发生
  • @Caleth:确实——虽然这意味着size() 内部的竞争不太可能,但我预计容器内的少量状态会被此操作修改,尽管这可能不会更多与整数递增/递减运算相比,坦率地说,我会惊讶于目睹真正的问题。但是,仍然需要规范的代码......
  • @Caleth:实际上,如果您正在执行一些批处理操作也许您可能会遇到size += n 的情况,然后就会出现问题。但是列表界面会竭尽全力阻止你做那样的事情
  • 生产者push_back在一个空列表上,而消费者阅读size是一场比赛。
  • @Caleth 是的。再次,我试图谈论那场比赛可能的实际结果,但同样,这并不重要 - 答案只是不这样做
【解决方案2】:

没有

想象一下列表只有 1 个元素,而您的 2 个线程试图“同时”推送和弹出。

如果你有很多元素,那么从前面弹出一个元素不太可能与后面的指针交互,而在后面推送一个元素不太可能与前面的指针发生冲突,所以这两个操作 /should/ 是安全的就列表的完整性而言。

但是很明显,当你从前面弹出最后一个元素时,后面的指针也会更新,也就是说没有更多元素了,当你把一个元素推到后面并且列表为空时,那么前端指针已更新。你能保证这些操作的顺序,并且不会卡顿吗?如果大小为零,您有保护,但如果大小为 1,则没有。

我担心的另一件事是,由于 c++11 size() 是“即时”答案 O(0),而不是扫描 O(n),并且要执行此列表,请保持整数计数。如果 push 和 pop “同时”修改大小计数器,那么它们可能会卡住并最终得到不正确的大小值。如果 list 内部使用 size 的值作为“is_empty()”检查,可能是为了优化插入空列表或弹出最后一个元素,那么他们可能会被愚弄执行不适当的操作。

【讨论】:

    猜你喜欢
    • 2014-02-19
    • 1970-01-01
    • 2013-12-17
    • 1970-01-01
    • 1970-01-01
    • 2017-04-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多