我可以在 one-reader/one-writer 队列中用 volatile 替换 atomic 吗?
Can I replace the atomic with a volatile in an one-reader/one-writer queue?
让我们考虑以下使用链表实现的one-reader/one-writer队列。
struct queue {
queue() {
tail = head = &reserved;
n = 0;
}
void push(item *it) {
tail->next = it;
tail = it;
n++;
}
item* pop() {
while (n == used);
++used;
head = head->next;
return head;
}
item reserved;
item *tail, *head;
int used = 0;
std::atomic <int> n;
}
现在我发现使用 volatile int n
可以使我的编写器 运行 更快,但我不确定它是否保证 head = head->next
始终可以读取正确的值。
更新:如果在 tail->next
、n++
之间添加一个原子操作,即
会怎么样
void push(item *it) {
tail->next = it;
tail = it;
a.store(0, std::memory_order_release);
a.load(std::memory_order_acquire);
n++;
}
其中 a
从未被 reader 访问过?这样能保证tail->next = it
和head = head->next
的顺序吗? (不过,它 运行s 比使用 atomic n
快)
C++ 中的 volatile
关键字不是为变量 read/write 提供保证在多线程环境中按照代码中的顺序排列的结构。因此,在您的代码中,原子模板包装计数器仅使用 volatile
关键字就可以了,增加消费者线程观察到的计数器并不能保证 item::next
也已更新。
为了在保证的情况下实现最大性能,我认为至少你必须在更新 head->next
和计数器的增量之间插入一个写屏障,例如通过 n.fetch_add(1, std::memory_order_release)
,以及在获取 tail->next
之前的读取屏障,例如 n.load(std::memory_order_acquire)
。不过,我不知道 CPU-arch 的具体细节。
正如其他几条评论中已经指出的那样,volatile 与多线程无关,因此这里应该不使用它。然而,volatile 比 atmoic 表现更好的原因很简单,因为 volatile ++n
转换为简单的加载、inc、存储指令,而 atomic 则转换为更昂贵的 lock xadd
(假设你为 x86 编译)。
但由于这只是一个 reader 单写入器队列,因此您不需要昂贵的读取-修改-写入操作:
struct queue {
queue() {
tail = head = &reserved;
n = 0;
}
void push(item *it) {
tail->next = it;
tail = it;
auto new_n = n.load(std::memory_order_relaxed) + 1;
n.store(new_n, std::memory_order_release);
}
item* pop() {
while (n.load(std::memory_order_acquire) == used);
++used;
head = head->next;
return head;
}
item reserved;
item *tail, *head;
int used = 0;
std::atomic <int> n;
}
这应该与易变版本一样好。如果acquire-load在pop
"sees" store-release写入的值在push
,两个操作同步,从而建立所需的happens-before关系。
让我们考虑以下使用链表实现的one-reader/one-writer队列。
struct queue {
queue() {
tail = head = &reserved;
n = 0;
}
void push(item *it) {
tail->next = it;
tail = it;
n++;
}
item* pop() {
while (n == used);
++used;
head = head->next;
return head;
}
item reserved;
item *tail, *head;
int used = 0;
std::atomic <int> n;
}
现在我发现使用 volatile int n
可以使我的编写器 运行 更快,但我不确定它是否保证 head = head->next
始终可以读取正确的值。
更新:如果在 tail->next
、n++
之间添加一个原子操作,即
void push(item *it) {
tail->next = it;
tail = it;
a.store(0, std::memory_order_release);
a.load(std::memory_order_acquire);
n++;
}
其中 a
从未被 reader 访问过?这样能保证tail->next = it
和head = head->next
的顺序吗? (不过,它 运行s 比使用 atomic n
快)
C++ 中的 volatile
关键字不是为变量 read/write 提供保证在多线程环境中按照代码中的顺序排列的结构。因此,在您的代码中,原子模板包装计数器仅使用 volatile
关键字就可以了,增加消费者线程观察到的计数器并不能保证 item::next
也已更新。
为了在保证的情况下实现最大性能,我认为至少你必须在更新 head->next
和计数器的增量之间插入一个写屏障,例如通过 n.fetch_add(1, std::memory_order_release)
,以及在获取 tail->next
之前的读取屏障,例如 n.load(std::memory_order_acquire)
。不过,我不知道 CPU-arch 的具体细节。
正如其他几条评论中已经指出的那样,volatile 与多线程无关,因此这里应该不使用它。然而,volatile 比 atmoic 表现更好的原因很简单,因为 volatile ++n
转换为简单的加载、inc、存储指令,而 atomic 则转换为更昂贵的 lock xadd
(假设你为 x86 编译)。
但由于这只是一个 reader 单写入器队列,因此您不需要昂贵的读取-修改-写入操作:
struct queue {
queue() {
tail = head = &reserved;
n = 0;
}
void push(item *it) {
tail->next = it;
tail = it;
auto new_n = n.load(std::memory_order_relaxed) + 1;
n.store(new_n, std::memory_order_release);
}
item* pop() {
while (n.load(std::memory_order_acquire) == used);
++used;
head = head->next;
return head;
}
item reserved;
item *tail, *head;
int used = 0;
std::atomic <int> n;
}
这应该与易变版本一样好。如果acquire-load在pop
"sees" store-release写入的值在push
,两个操作同步,从而建立所需的happens-before关系。