【发布时间】:2019-09-10 02:26:38
【问题描述】:
以下代码实现了一些无锁(且无原子!)的线程间通信,需要使用存储和加载内存屏障,但 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 #StoreStore 和 membar #LoadLoad 上,在 PowerPC lwsync 上都适用。所以没什么大不了的,代码是使用存储和加载障碍的典型示例。现在,假设我不想将“blob”转换为std::atomic 对象,我应该使用什么 C++11 构造来使代码正确,因为它会使“blob”成为保护对象,变量“有效”成为受保护的对象一个,而反过来。
将变量 '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;
}
代码不正确,因为屏障放置在错误的位置,因此写入“blob”可以先于写入“valid”或/并且从“valid”加载可以先于从“blob”加载。我认为为了处理这种结构,C++11 提供了std::atomic_thread_fence,代码应该是:
volatile std::atomic<bool> valid{true};
volatile uint8_t blob[1024] = {/*some values*/};
void zero_blob() {
valid.store(false, std::memory_order_relaxed);
std::atomic_thread_fence(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];
std::atomic_thread_fence(std::memory_order_acquire);
return valid.load(std::memory_order_relaxed); ? res : -1;
}
不幸的是 C++11 说:
如果存在,释放栅栏 A 与获取栅栏 B 同步 原子操作 X 和 Y,都对某个原子对象 M 进行操作, 使得 A 在 X 之前排序,X 修改 M,Y 在之前排序 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;
}
【问题讨论】:
-
我认为您的代码中有一个 data race,根据 C++ 标准它是 UB。什么是
memsetting 和阅读blob[index]同时发生?标准并没有说res将是未指定,而是clearly says that this is UB。当然,它可能适用于您的实现/环境,但我建议不要使用此类代码。 -
volatileis not useful to any of your code. 如果你将atomic的值设为volatile,那么你几乎肯定做错了。 -
volatile std::atomic<bool>嗯...这是一个新的添加到我的 volatile 滥用列表中 -
@NicolBolas volatile 原子变量有什么“错误”?
-
“更有用的例子”本质上是“SeqLock”。可以为 SINGLE writer 实现,多个 reader 无需锁定,但我认为继续执行 mutex lock 写入会更好。如果只有一个写入者,则永远不会争用锁,因此它相对免费,并且可以防止出现多个写入者时出现问题。网络搜索“SeqLock”获取大量信息,以及一些实现。
标签: c++ multithreading c++11 memory-barriers stdatomic