没有std :: atomics的无锁哈希是否保证在C ++ 11中是线程安全的?

时间:2012-09-20 07:16:07

标签: c++ multithreading c++11 atomic lockless

考虑以下针对多线程搜索算法的无锁散列表的尝试(受此paper启发)

struct Data
{
    uint64_t key;
    uint64_t value;
};

struct HashEntry
{
    uint64_t key_xor_value;
    uint64_t value;
};

void insert_data(Data const& e, HashEntry* h, std::size_t tableOffset)
{
    h[tableOffset].key_xor_value = e.key ^ e.value;
    h[tableOffset].value = e.value;
}

bool data_is_present(Data const& e, HashEntry const* h, std::size_t tableOffset)
{
    auto const tmp_key_xor_value = h[tableOFfset].key_xor_value;
    auto const tmp_value = h[tableOffset].value;

    return e.key == (tmp_key_xor_value ^ tmp_value);
}

这个想法是HashEntry结构存储Data结构的两个64位字的异或组合。如果两个线程对HashEntry结构的两个64位字进行交错读/写,那么想法是读取线程可以通过再次进行异或并与原始{{1}进行比较来检测}。因此,损坏的哈希条目可能会导致效率降低,但如果解码后的检索密钥与原始密钥匹配,则仍然保证正确性。

该文件提到它基于以下假设:

  

对于本讨论的其余部分,假设64位内存   读/写操作是原子的,即整个64位值   在一个周期内读/写。

我的问题是:上面的代码是否没有明确使用key保证在C ++ 11中是线程安全的?或者可以通过同时读/写来破坏各个64位字?即使在64位平台上?这与旧的C ++ 98标准有何不同?

非常感谢标准中的引用。

更新:基于Hans Boehm on "benign" data races这篇精彩的论文,一个简单的方法就是让编译器取消来自std::atomic<uint64_t>insert_data()的XOR总是返回data_is_present(),例如如果它找到像

这样的本地代码片段
true

3 个答案:

答案 0 :(得分:7)

C ++ 11规范几乎定义了一个线程读取或写入另一个线程正在写入的内存位置的任何尝试作为未定义的行为(没有使用原子或互斥锁来防止来自一个线程的读/写)另一个主题是写作。)

个别编译器可能会使其安全,但C ++ 11规范本身并不提供覆盖。同时读取从来都不是问题;它是在一个线程中写入而在另一个线程中读写。

  

这与旧的C ++ 98标准有何不同?

C ++ 98/03标准不提供有关线程的任何覆盖。就C ++ 98/03内存模型而言,threading is not a thing that can possibly happen

答案 1 :(得分:2)

我不认为它在很大程度上取决于你正在使用的CPU(它的指令集)上的编译器。我不认为这个假设是非常便携的。

答案 2 :(得分:1)

代码完全坏了。如果编译器的分析表明整体效果相同,则编译器可以自由地重新排序指令。例如,在insert_data中,无法保证key_xor_value将在value之前更新,无论更新是在临时寄存器上完成还是写回缓存之前,更不用说那些缓存了更新 - 无论机器代码语言和CPU指令执行管道中的“顺序” - 将从更新核心或核心(如果上下文切换的中间函数)私有高速缓存中刷新,以便其他线程可见。编译器甚至可以使用32位寄存器逐步执行更新,具体取决于CPU,是否编译32位或64位,编译选项等。

原子操作往往需要CAS(比较和交换)样式指令,或volatile和内存屏障指令,它们可以跨核心的缓存同步数据并强制执行某些排序。