如何在C++11中表示普通存储(导出)和加载(导入)屏障(围栏)?

如何在C++11中表示普通存储(导出)和加载(导入)屏障(围栏)?,c++,multithreading,c++11,memory-barriers,stdatomic,C++,Multithreading,C++11,Memory Barriers,Stdatomic,下面的代码实现了一些无锁和原子自由!线程间通信需要使用存储和加载内存屏障,但C++11发布获取语义不合适,也不能保证正确性。实际上,该算法揭示了对释放-获取语义的一种反转的需要,即,表示某些操作没有发生,而不是发生了 volatile bool valid=true; volatile uint8_t blob[1024] = {/*some values*/}; void zero_blob() { valid=false; STORE_BARRIER; memset

下面的代码实现了一些无锁和原子自由!线程间通信需要使用存储和加载内存屏障,但C++11发布获取语义不合适,也不能保证正确性。实际上,该算法揭示了对释放-获取语义的一种反转的需要,即,表示某些操作没有发生,而不是发生了

volatile bool valid=true;
volatile uint8_t blob[1024] = {/*some values*/};

void zero_blob() {
    valid=false;
    STORE_BARRIER;
    memset(blob,0,1024);
}

int32_t try_get_sum(size_t index_1, size_t index_2) {
    uint8_t res = blob[index_1] + blob[index_2];
    LOAD_BARRIER;
    return valid ? res : -1; 
}
我可以简单地使用本机内存屏障在所有硬件体系结构上更正此代码,例如,在Intel上,在Sparc RMO membar Store和membar LoadLoad上,在PowerPC lwsync上,这里不需要内存屏障。所以这没什么大不了的,代码是使用存储和加载屏障的典型示例。现在,假设我不想将'blob'转换为std::atomic对象,因为它会使'blob'成为保护对象,而变量'valid'则成为保护对象,那么我应该使用什么样的C++11构造来使代码正确呢。 将变量“valid”转换为std::atomic对象对我来说是可以的,但是没有任何障碍来保证正确性。为了说明清楚,让我们考虑下面的代码:

volatile std::atomic<bool> valid{true};
volatile uint8_t blob[1024] = {/*some values*/};

void zero_blob() {
    valid.store(false, std::memory_order_release);
    memset(blob,0,1024);
}

int32_t try_get_sum(size_t index_1, size_t index_2) {
    uint8_t res = blob[index_1] + blob[index_2];
    return valid.load(std::memory_order_acquire) ? res : -1; 
}
不幸的是,C++11说:

如果存在,释放栅栏A与获取栅栏B同步 原子操作X和Y,都在某个原子对象M上操作, 这样A在X之前排序,X修改M,Y在X之前排序 B、 Y读取X写的值或任意边写的值 假设释放序列X中的效应如果为 释放操作

这清楚地表明std::atomic_thread_fence应该放置在原子对象上的操作的对面

后期编辑

下面请找到更有用的示例:

volatile uint64_t clock=1;
volatile uint8_t blob[1024] = {/*some values*/};

void update_blob(uint8_t vals[1024]) {
    clock++;
    STORE_BARRIER;
    memcpy(blob,vals,1024);
    STORE_BARRIER;
    clock++;
}

int32_t try_get_sum(size_t index_1, size_t index_2) {
    uint64_t snapshot = clock;
    if(snapshot & 0x1) {
        LOAD_BARRIER;
        uint8_t res = blob[index_1] + blob[index_2];
        LOAD_BARRIER;
        if(snapshot == clock)
            return res;
    }
    return -1;
}
根据,为了保守安全,您需要在存储之后使用memory\u order\u release,在加载相同的原子变量之前使用memory\u order\u acquire

因此:

更一般地说,根据您需要实现的效果和您的目标体系结构支持的内容,您可以不那么保守地实现它

您通常会选择std::atomic,因为前者保证是无锁的,而后者则不同


最后-为什么不从受互斥保护的关键部分开始,或者更好,通过无锁环形缓冲区将更新推送到使用者,这样他们就不会共享任何内容?

您的示例是正确的。我不认为标准是非常明确的,当涉及到如何处理非原子

std::atomic<bool> valid{true};           // removed volatile
uint8_t blob[1024] = {/*some values*/};  // removed volatile

void zero_blob() {
    valid.store(false, std::memory_order_relaxed);            // A)
    std::atomic_thread_fence(std::memory_order_release);      // B)
    memset(blob,0,1024);                                      // C)
}                                                              

int32_t try_get_sum(size_t index_1, size_t index_2) {          
    uint8_t res = blob[index_1] + blob[index_2];              // D)
    std::atomic_thread_fence(std::memory_order_acquire);      // E)
    return valid.load(std::memory_order_relaxed) ? res : -1;  // F)
}
步骤B中的围栏保证程序顺序将得到遵守,标准并没有真正说明这一点,但在同一线程中的任何后续写入之前,应该传播存储到valid

步骤E中的围栏保证遵守计划顺序

因此,线程间发生在C之前,D发生在F之前

如果F认为valid是真的,那么,假设没有ABA问题,F发生在A之前

如果F发生在A之前,那么D发生在A之前

如果F认为valid为false,那么A发生在F之前。这并不一定意味着C发生在D之前,但我们丢弃结果以防万一。丢弃确实很重要,因为blob中的值可能完全无效。尽管使用uint8\u t可以防止从转换到void*的部分读取。它是缓存对齐的,但从技术上讲它不是字大小的,这就使原子性受到质疑。IIRC从理论上讲,它也可能容易受到某些不太可能的架构上的真正部分读取的影响

这些推导是基于Linux内核内存一致性模型LKMM,而不是C++内存模型。从我在CPPyrExcE.com上读到的,当他们谈论C++内存模型中的传播顺序时,他们使用的短语,比如在线程A中的X之前的所有变化,在线程B中Y之后都可见,但通常仅在原子操作的上下文中。我认为,希望C++编译器将发出指令,保证传播顺序,至少在消费级CPU上是相对低风险的。您仍然应该检查程序集并查阅CPU文档。多亏了TSO,您在x86上肯定会很好,但我从未注意过弱有序体系结构上有哪些传播顺序原语可用

最后,如果要重新使用blob,那么应该有另一个原子变量来指示memset已经完成

下面请找到更有用的示例:

volatile uint64_t clock=1;
volatile uint8_t blob[1024] = {/*some values*/};

void update_blob(uint8_t vals[1024]) {
    clock++;
    STORE_BARRIER;
    memcpy(blob,vals,1024);
    STORE_BARRIER;
    clock++;
}

int32_t try_get_sum(size_t index_1, size_t index_2) {
    uint64_t snapshot = clock;
    if(snapshot & 0x1) {
        LOAD_BARRIER;
        uint8_t res = blob[index_1] + blob[index_2];
        LOAD_BARRIER;
        if(snapshot == clock)
            return res;
    }
    return -1;
}
请允许我重申一下你基本上在做什么

您的代码正在从内存中读取。该内存可以随时由其他线程更新。但您不希望强制执行同步,即读写器之间的某种互斥。因此,您构建了一个系统,该系统允许您检测您已经执行的读取是否已被其他thre覆盖 如果是,你只需忽略你读到的值

C++不允许您这样做

如果您正在读取可能由其他线程写入的非原子对象,而这两个操作之间没有适当的执行和内存同步,那么您就存在数据竞争。数据竞争的存在并不意味着您可能读取不正确的值;这意味着您的代码具有未定义的行为

您无法撤消UB。一旦您的程序进入未定义的行为区域,所有赌注都将被取消。至少,就标准而言

当然,您可以重写代码以在标准范围内工作。但它必须防止在可能发生写入时执行读取,而不仅仅是事后检查读取是否正常。如果写入总是1KB,则基于原子的自旋锁可能适用于写入函数,如果原子锁不可用,读取器可以返回-1

你也可以写这种系统,显然是使用C++原子,它完全和完整的知识,它调用UB到标准。只要您复制的类型很简单,它就可以正常工作


注意,C++并发Ts 2会。这将有望看到C++23的完整标准。

std::atomic_flag需要atomic_flag_INIT,但它有一些缺点。@MaxLanghof-是的,没有免费的cheeseNo,我通常更喜欢std::atomic而不是std::atomic_标志,因为我不希望在代码中使用过多的原子指令来加载和存储值。@bobah我想你还没有理解我描述的问题。不可能得到STD::在我的例子中,原子对象和释放都有障碍,我认为你的代码中有一个数据竞赛,它是根据C++标准的UB。什么是memsetting和reading blob[index]同时发生?该标准并没有说res将是未指定的,而是说。当然,它可能适用于您的实现/环境,但我建议不要使用此类代码。如果你让一个原子值不稳定,那么你几乎肯定是做错了。。。这是一个新的添加到我的滥用列表volatile@NicolBolas易失性原子变量有什么问题?更有用的示例本质上是SeqLock。可以在不锁定的情况下实现单个编写器、多个读卡器,但我认为最好继续使用互斥锁来锁定编写。如果只有一个writer,锁将永远不会被争用,因此它相对来说是免费的,并且可以防止出现多个writer时出现问题。Web搜索SeqLock以获取大量信息和许多实现。100%正确,我要补充不幸的是,因为这种结构适用于我所知道的所有硬件,包括Intel、Sparc、PowerPC,它是Linux内核中使用的SeqLock的基础,也是软件事务性内存的最快算法。请注意,唯一错误的是并发数据访问超出STD::原子,C++ 11故意变成了不确定的行为,遵循理论内存模型和外来硬件,与前沿的C++应用程序几乎没有共同之处。此外,以前的许多C++11多线程代码都不正确,就像我的例子一样。@dervih:你的例子都不正确;他们只是你侥幸逃脱的。也就是说,它们是在编译成机器代码时发生的事情,这些机器代码似乎能满足您的需要。这仍然是真正的C++ 11作为C++的标准,但是你不认为这实际上是C++标准委员会的失败,在C++ 11之前,几乎所有的多线程代码都依赖于编译器和硬件运行正常,并且C++ 11的小代码已经改变了,特别是当涉及到现有的高性能/低延迟/可扩展应用程序时。C++不是为应用程序选择的,因为它是一种很好的语言,但它却能让你从机器的100%中获得能量。因此,令人遗憾的是,一些高性能技术遇到了未定义的行为。无论如何,C++终于有了一些内存模型,非常高兴。@德维希:用C++ 11,小的已经改变了,我不买。在C++11之前,您根本无法按照标准编写具有良好定义行为的线程代码。有能力写出定义明确的东西是一种进步。它可能不允许一切,当然也不能使您的旧特别代码正确,但这并不意味着没有太大的变化。即使是这个特定的问题也正在被审查。伟大:似乎投机阅读最终有机会在C++中成为合法的,所有依赖于它们的快速软件事务内存算法不会再被谴责。你们让我度过了一天,因为现在我有一个论点,这种方法被认真地认为是一种彻底的编程技术。谢谢
std::atomic<bool> valid{true};           // removed volatile
uint8_t blob[1024] = {/*some values*/};  // removed volatile

void zero_blob() {
    valid.store(false, std::memory_order_relaxed);            // A)
    std::atomic_thread_fence(std::memory_order_release);      // B)
    memset(blob,0,1024);                                      // C)
}                                                              

int32_t try_get_sum(size_t index_1, size_t index_2) {          
    uint8_t res = blob[index_1] + blob[index_2];              // D)
    std::atomic_thread_fence(std::memory_order_acquire);      // E)
    return valid.load(std::memory_order_relaxed) ? res : -1;  // F)
}