关于c ++:没有std :: atomics的无锁散列是否保证在C ++ 11中是线程安全的?

Is lockless hashing without std::atomics guaranteed to be thread-safe in C++11?

考虑以下对多线程搜索算法的无锁哈希表的尝试(本文的启发)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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位字的xor-ed组合。如果两个线程对HashEntry结构的两个64位字进行了交叉读/写操作,那么读线程可以通过再次执行xor-ing并与原始key进行比较来检测这一点。因此,如果散列条目被破坏,可能会降低效率,但如果解码后的检索密钥与原始密钥匹配,仍然可以保证正确性。

本文提到,它基于以下假设:

For the remainder of this discussion, assume that 64 bit memory
read/write operations are atomic, that is the entire 64 bit value is
read/written in one cycle.

我的问题是:上面的代码没有显式地使用EDCOX1(4)保证在C++ 11中是线程安全的吗?或者单个64位字可以被同时读/写损坏吗?即使在64位平台上?这与旧的C++ 98标准有什么不同呢?

本标准的报价将不胜感激。

更新:根据HansBoehm关于"良性"数据竞争的这篇令人惊叹的文章,一个简单的方法就是让编译器取消insert_data()data_is_present()的两个xor,返回true,例如,如果它找到一个类似于

1
2
3
insert_data(e, h, t);
if (data_is_present(e, h, t)) // optimized to true as if in single-threaded code
   read_and_process(e, h, t); // data race if other thread has written


C++ 11规范几乎定义了一个线程试图读取或写入另一个线程正在写入的内存位置的尝试,这是由于未定义的行为(没有使用原子或互斥体来防止一个线程在另一个线程写入时的读/写)。

单独的编译器可以使其安全,但是C++ 11规范本身不提供覆盖。同时读从来不是问题;它是在一个线程中写,而在另一个线程中读/写。

And how is this different from the old C++98 Standard?

C++ 98/03标准没有提供关于线程的任何覆盖。就C++ 98/03内存模型而言,线程并不是一种可能发生的事情。


我认为它不太依赖于编译器,而是依赖于您正在使用的CPU(它的指令集)。我不认为这个假设是非常可移植的。


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

原子操作往往需要类似于cas(比较和交换)类型的指令,或者volatile和内存屏障指令,这些指令可以在核心缓存中同步数据并强制执行某些排序。