- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
鉴于以下示例打算等待另一个线程存储 42
在共享变量中 shared
没有锁也没有等待线程终止,为什么会 volatile T
或 std::atomic<T>
需要或推荐以保证并发正确性?
#include <atomic>
#include <cassert>
#include <cstdint>
#include <thread>
int main()
{
int64_t shared = 0;
std::thread thread([&shared]() {
shared = 42;
});
while (shared != 42) {
}
assert(shared == 42);
thread.join();
return 0;
}
使用 GCC 4.8.5 和默认选项,示例按预期工作。
最佳答案
测试似乎表明样本是正确的但它不是 .类似的代码很容易在生产中结束,甚至可以完美运行多年。
我们可以从编译样本开始 -O3
.现在,样本无限期地挂起。 (默认是 -O0
,没有优化/调试一致性,这有点类似于让每个变量 volatile
, which is the reason the test didn't reveal the code as unsafe 。)
为了找到根本原因,我们必须检查生成的程序集。一、GCC 4.8.5 -O0
基于 x86_64 程序集对应于未优化的工作二进制文件:
// Thread B:
// shared = 42;
movq -8(%rbp), %rax
movq (%rax), %rax
movq $42, (%rax)
// Thread A:
// while (shared != 42) {
// }
.L11:
movq -32(%rbp), %rax # Check shared every iteration
cmpq $42, %rax
jne .L11
线程 B 执行值
42
的简单存储在
shared
.
shared
对于每次循环迭代,直到比较表明相等。
-O3
进行比较。结果:
// Thread B:
// shared = 42;
movq 8(%rdi), %rax
movq $42, (%rax)
// Thread A:
// while (shared != 42) {
// }
cmpq $42, (%rsp) # check shared once
je .L87 # and skip the infinite loop or not
.L88:
jmp .L88 # infinite loop
.L87:
与
-O3
相关的优化用单个比较替换循环,如果不相等,则用无限循环来匹配预期行为。使用 GCC 10.2,优化了循环。 (与 C 不同,没有副作用或 volatile 访问的无限循环在 C++ 中是未定义的行为。)
shared
不能在线程 A 中更改 - 循环相当于死代码。 (或者换句话说,数据竞争是 UB,并且优化器可以假设程序没有遇到 UB。如果您正在读取一个非原子变量,那一定意味着没有其他人在写它。这个是什么允许编译器从循环中提升负载,以及类似的接收器存储,这对于非共享变量的正常情况是非常有值(value)的优化。)
shared
参与线程间通信。实现这一点的一种方法可能是
volatile
.而
volatile
的实际含义因编译器而异,并且保证(如果有)是特定于编译器的,普遍的共识是
volatile
阻止编译器在基于寄存器的缓存方面优化 volatile 访问。这对于与硬件交互并在并发编程中占有一席之地的低级代码至关重要,尽管由于
std::atomic
的引入而呈下降趋势。 .
volatile int64_t shared
,生成的指令变化如下:
// Thread B:
// shared = 42;
movq 24(%rdi), %rax
movq $42, (%rax)
// Thread A:
// while (shared != 42) {
// }
.L87:
movq 8(%rsp), %rax
cmpq $42, %rax
jne .L87
由于必须假设
shared
,因此无法再消除循环。即使没有代码形式的证据也改变了。因此,该示例现在适用于
-O3
.
volatile
解决了这个问题,你为什么需要
std::atomic
?与无锁代码相关的两个方面是
std::atomic
本质:内存操作原子性和内存顺序。
-O3 -m32
(32 位版本)适用于
volatile int64_t shared
:
// Thread B:
// shared = 42;
movl 4(%esp), %eax
movl 12(%eax), %eax
movl $42, (%eax)
movl $0, 4(%eax)
// Thread A:
// while (shared != 42) {
// }
.L88: # do {
movl 40(%esp), %eax
movl 44(%esp), %edx
xorl $42, %eax
movl %eax, %ecx
orl %edx, %ecx
jne .L88 # } while(shared ^ 42 != 0);
对于 32 位 x86 代码生成,64 位加载和存储通常分为两条指令。对于单线程代码,这不是问题。对于多线程代码,这意味着另一个线程可以看到 64 位内存操作的部分结果,为意外的不一致留出空间,这些不一致可能不会 100% 的时间导致问题,但可能会随机发生并且出现概率受周围代码和软件使用模式的严重影响。即使 GCC 选择生成默认保证原子性的指令,这仍然不会影响其他编译器,并且可能不适用于所有支持的平台。
std::atomic
可以就业。让我们回顾一下如何
std::atomic
影响生成的程序集。更新的样本:
#include <atomic>
#include <cassert>
#include <cstdint>
#include <thread>
int main()
{
std::atomic<int64_t> shared;
std::thread thread([&shared]() {
shared.store(42, std::memory_order_relaxed);
});
while (shared.load(std::memory_order_relaxed) != 42) {
}
assert(shared.load(std::memory_order_relaxed) == 42);
thread.join();
return 0;
}
基于 GCC 10.2 (
-O3
:
https://godbolt.org/z/8sPs55nzT ) 生成的 32 位程序集:
// Thread B:
// shared.store(42, std::memory_order_relaxed);
movl $42, %ecx
xorl %ebx, %ebx
subl $8, %esp
movl 16(%esp), %eax
movl 4(%eax), %eax # function arg: pointer to shared
movl %ecx, (%esp)
movl %ebx, 4(%esp)
movq (%esp), %xmm0 # 8-byte reload
movq %xmm0, (%eax) # 8-byte store to shared
addl $8, %esp
// Thread A:
// while (shared.load(std::memory_order_relaxed) != 42) {
// }
.L9: # do {
movq -16(%ebp), %xmm1 # 8-byte load from shared
movq %xmm1, -32(%ebp) # copy to a dummy temporary
movl -32(%ebp), %edx
movl -28(%ebp), %ecx # and scalar reload
movl %edx, %eax
movl %ecx, %edx
xorl $42, %eax
orl %eax, %edx
jne .L9 # } while(shared.load() ^ 42 != 0);
为了保证加载和存储的原子性,编译器发出一个 8 字节
SSE2 movq
instruction (到/从 128 位 SSE 寄存器的下半部分)。此外,程序集显示即使
volatile
环仍保持完整去掉了。
std::atomic
在样本中,可以保证
Implementations should make atomic stores visible to atomic loads within a reasonable amount of time.
std::atomic
跨迭代加载,例如在我们的示例中触发(没有 volatile 或 atomic)显然是一种违规——存储可能永远不会变得可见。当前编译器
don't even optimize atomics within one block , 就像在同一次迭代中进行 2 次访问一样。
movq
.
atomic<T>
带大号
T
硬件可能不直接支持,在这种情况下编译器可以回退
to using a mutex .
T
(例如 2 个寄存器的大小)在某些平台上可能需要原子 RMW 操作(如果编译器不简单地回退到锁定),有时提供的大小比最大的有效纯加载/纯存储更大保证原子。 (例如,在 x86-64、
lock cmpxchg16
或 ARM
ldrexd
/
strexd
重试循环上)。单指令原子 RMW(如 x86 使用)
internally involve a cache line lock or a bus lock .例如,旧版本的
clang -m32
x86 将使用
lock cmpxchg8b
而不是
movq
用于 8 字节纯加载或纯存储。
std::memory_order_relaxed
意思?
std::atomic
指定所需的内存顺序加载和存储。
std::memory_order_relaxed
不强加任何特定的顺序。
std::atomic<T>
与
std::memory_order_relaxed
(除非场景要求特定的内存顺序)到
volatile T
(当然还有
T
)。此外,只要有可能,最好不要推出自己的无锁代码,以降低代码复杂性并最大限度地提高正确性的概率。
关于c++ - T、volatile T 和 std::atomic<T> 之间有什么区别?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66789935/
我正在开发一个小型图书馆,我需要做的一件事是让访问者访问一些数据并返回结果。 在一些较旧的 C++ 代码中,访问者需要声明一个 typedef return_type .例如,boost::stati
我正在尝试使用std:map类型的键和值制作std::any Visual Studio 2017 std::map m("lastname", "Ivanov"); std::cout (m["la
我已经在 C++ 的 map 中声明了一个集合为 std::map> .如何循环访问或打印设定值? 最佳答案 如果你知道如何迭代 std::map或 std::set单独地,您应该可以毫无问题地组合迭
如何循环? 我已经试过了: //----- code std::vector >::iterator it; for ( it = users.begin(); it != users.end();
我有两个用例。 A.我想同步访问两个线程的队列。 B.我想同步两个线程对队列的访问并使用条件变量,因为其中一个线程将等待另一个线程将内容存储到队列中。 对于用例 A,我看到了使用 std::lock_
我正在查看这两种类型特征的文档,但不确定有什么区别。我不是语言律师,但据我所知,它们都适用于“memcpy-able”类型。 它们可以互换使用吗? 最佳答案 不,这些术语不能互换使用。这两个术语都表示
我有以下测试代码,其中有一个参数 fS,它是 ofstream 的容器: #include #include #include #include int
这是这个问题的延续 c++ function ptr in unorderer_map, compile time error 我试图使用 std::function 而不是函数指针,并且只有当函数是
std::unordered_map str_bool_map = { {"a", true}, {"b", false}, {"c", true} }; 我们可以在此映射上使
我有以下对象 std::vector> vectorList; 然后我添加到这个使用 std::vector vec_tmp; vec_tmp.push_back(strDRG); vec_tmp.p
为什么 std::initializer_list不支持std::get<> , std::tuple_size和 std::tuple_element ?在constexpr中用得很多现在的表达式,
我有一个像这样定义的变量 auto drum = std::make_tuple ( std::make_tuple ( 0.3f , Ex
假设我有一个私有(private)std::map在我的类(class)里std::map 。我怎样才能将其转换为std::map返回给用户?我想要下面的原型(prototype) const std
假设我有一个私有(private)std::map在我的类(class)里std::map 。我怎样才能将其转换为std::map返回给用户?我想要下面的原型(prototype) const std
问题 我正在尝试将 lambda 闭包传递给 std::thread,它使用任意封闭参数调用任意封闭函数。 template std::thread timed_thread(Function&& f
我想创建一个模板类,可以容纳容器和容器的任意组合。例如,std::vector或 std::map ,例如。 我尝试了很多组合,但我必须承认模板的复杂性让我不知所措。我编译的关闭是这样的: templ
我有一个 std::vector>我将其分配给相同类型的第二个 vector 。 我收到这个编译器错误: /opt/gcc-8.2.0/include/c++/8.2.0/bits/stl_algob
有时候,我们有一个工厂可以生成一个 std::unique_ptr vector ,后来我们想在类/线程/你命名的之间共享这些指针。因此,最好改用 std::shared_ptr 。当然有一种方法可以
这个问题在这里已经有了答案: Sorting a vector of custom objects (14 个答案) 关闭 6 年前。 我创建了一个 vector vector ,我想根据我定义的参
我有三个类(class)成员: public: std::vector > getObjects(); std::vector > getObjects() const; privat
我是一名优秀的程序员,十分优秀!