【问题标题】:The same instances of the same class but different behaviour. Probable UB同一类的相同实例,但行为不同。可能的 UB
【发布时间】:2016-08-10 08:42:25
【问题描述】:
#include <iostream>

#include <atomic>
#include <memory>


template<typename T>
class LockFreeQueue {
public:
    struct CountedNode;

private:
    std::atomic<CountedNode> head;
public:
    struct Node{
        explicit Node(const T& d) : next(CountedNode()), data(std::make_shared<T>(d)), node_counter(0) { }
        std::atomic<CountedNode> next;
        std::shared_ptr<T> data;
        std::atomic<unsigned> node_counter;
    };
    struct CountedNode {
        CountedNode() noexcept : node(nullptr), counter(0) {}
        explicit  CountedNode( const T& data) noexcept : node(new Node(data) /* $4 */), counter(0)  {}
        Node* node;
        int counter;
    };


    void push( const T& data)
    {
        CountedNode new_node(data), curr, incrementedNext, next /*($2) */;
        CountedNode empty; /*($3) */
        if (head.compare_exchange_strong(empty, new_node)) std::cout << "EQUALS\n"; // $1
        else std::cout << "NOT EQUALS\n";

        if (head.compare_exchange_strong(next, new_node)) std::cout << "EQUALS\n"; // $1
        else std::cout << "NOT EQUALS\n";
    }

};


int main() {
    LockFreeQueue<int> Q;
    Q.push(2);

    return 0;
}



    int main(){
    LockFreeQueue<int> Q;
    Q.push(2);

    return 0;
    }

好的。它编译并执行没有错误。但是,仍然存在问题,我将在下面描述。

http://coliru.stacked-crooked.com/a/1fe71fafc5dde518

在我看来,结果不是预期的: 注意事项 等于

我对上面的代码有一个很大的问题。

特别是,$1 行中的比较让我很头疼。我的意思是,这个比较总是返回 false,尽管它应该在第一次返回 true。

我很困惑,所以我查看了emptyhead 的内存,实际上它们是不同的。 head 等于 0x00000000 0x00000000 0x00000000 0x00000000 (当涉及到字节时),它似乎没问题。但是empty 等于: 0x00000000 0x00000000 0x00000000 0x7f7f7f7f7f。更有趣的是$2中的next等于0x00000000 0x00000000 0x00000000 0x00000000所以实际上等于head。但是,例如,currincrementedNext 等于 0x00000000 0x00000000 0x00000000 0x7f7f7f7f7f。 所以这种行为是不确定的,所以我想有任何未定义的行为,但为什么呢?我做错了什么,请解释一下这种行为。

附:我知道$4 中的内存泄漏,但现在我忽略了它。

我编译它: g++ -latomic main.cpp -std=c++14。 我的 gcc 版本是 6.1.0。我也在 gcc 5.1.0 上进行了测试。结果是一样的。

@PeterCordes 创建的源链接:https://godbolt.org/g/X02QV8

【问题讨论】:

  • 好吧,您可以随时编辑原始帖子,直到它正确为止。
  • 我只是复制/粘贴您的代码并编译它,它适用于我:example。我可以建议您编写编译器版本和编译代码的命令吗?
  • @Gilgamesz 我刚刚用g++ -m32 -g -o test test.cpp -latomic 编译(gcc 6.1)并且它工作正常。使用调试器并查看内存时尝试启用调试标志(-g)。
  • @BiagioFesta: Baseline x86-64 没有cmpxchg16b,因此需要使用-mcx16 来启用它(或者它作为-march=haswell 的一部分启用,或者-march=native on除了最古老的 CPU 之外的任何东西)。如果您不这样做,gcc 会发出对库函数的调用。您可以使用-latomic 来避免链接错误。

标签: c++ c++11 x86 undefined-behavior stdatomic


【解决方案1】:

填充。 std::atomic::compare_exchange* 比较两个对象的内存表示,就像通过memcmp 一样。如果结构有填充,则其内容是不确定的,并且可能使两个实例看起来不同,即使它们在成员方面相等(请注意,CountedNode 甚至没有定义 operator==)。

在 64 位版本中,counter 之后有填充,您会看到问题所在。在 32 位版本中,没有,您也没有。

编辑:我现在相信下面的部分是错误的;仅保留完整性。 std::atomic_init 没有做任何事情来将填充归零;该示例似乎只是偶然地起作用。


head(以及Node::next)应该用std::atomic_init初始化:

std::atomic_init(&head, CountedNode());

有了这个,your example works as expected

【讨论】:

  • 它不起作用。问题依然存在。您的链接之所以有效,只是因为填充的内容是不确定的,并且恰好在您的代码段empty 的填充等于零。但是,如果您更改堆栈视图(请参阅链接我的意思,(35 行))您将再次看到NOT EQUAL,请参阅:coliru.stacked-crooked.com/a/d6a5c66734862e91 我尝试通过“手动”写入empty 来解决它我的记忆,它有帮助,我的意思是:coliru.stacked-crooked.com/a/56c9c69622fb3492
  • 1.在那种情况下,std::atomic_init 是如何工作的,是否有必要? 2. 如何解决这个问题比手动写入填充更好?
  • 对。事实上,我怀疑 std::atomic_init 并没有真正帮助 - 它只是 memcpy 将其参数放入原子中,但没有任何内容表明该参数的填充为零。所以基本上,看起来像std::atomic&lt;T&gt; 其中T 不是整数或指针类型是一个坏主意。
  • 为特定平台编写时,您可以以一种可能避免填充的方式设计您的结构(例如,使用uintptr_t 代替counter,因此它的大小与指针相同,这应该消除大多数编译器的填充)。但总的来说,我想不出一种方法来便携式使用std::atomic&lt;SomeStructure&gt;。它根本不是为此而设计的。
  • 这是在[atomics.types.operations.req]/27 中明确指出的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-16
  • 1970-01-01
  • 2011-03-08
  • 2017-01-17
  • 2013-09-12
相关资源
最近更新 更多