取消引用指针总是会导致内存访问吗?

Yan*_*Zai 21 c c++ multithreading pointers

我想知道无论编译器如何优化,取消引用指针是否总是会被转换为机器级加载/存储指令。

假设我们有两个线程,一个(我们称之为 Tom)接收用户输入并写入一个bool变量。该变量由另一个人(这是 Jerry)读取来决定是否继续循环。我们知道优化编译器在编译循环时可能会将变量存储在寄存器中。因此,在运行时,Jerry 可能会读取与 Tom 实际写入的值不同的过时值。因此,我们应该将bool变量声明为volatile

但是,如果引用指针总是会导致内存访问,那么两个线程可以使用指针来引用该变量。每次写入时,Tom 都会通过取消引用指针并将新值写入内存。每次读取时,Jerry 都可以通过取消引用同一指针来真正读取 Tom 所写的内容。这似乎比依赖于实现的更好volatile

我是多线程编程的新手,所以这个想法可能看起来微不足道且不必要。但我真的很好奇。

Jan*_*tke 30

取消引用指针总是会导致内存访问吗?

不,例如:

int five() {
    int x = 5;
    int *ptr = &x;
    return *ptr;
}
Run Code Online (Sandbox Code Playgroud)

任何理智的优化编译器都不会mov在这里从堆栈内存中发出 a ,而是发出类似以下内容的内容:

five():
  mov eax, 5
  ret
Run Code Online (Sandbox Code Playgroud)

由于假设规则,这是允许的。

如何通过bool*then 进行线程间通信?

这就是std::atomic<bool>目的。您不应该使用非原子对象在线程之间进行通信,因为通过两个线程以冲突的方式访问同一内存位置1)是 C++ 中未定义的行为。std::atomic使其成为线程安全的,volatile但事实并非如此。例如:

five():
  mov eax, 5
  ret
Run Code Online (Sandbox Code Playgroud)

从技术上讲,这并不意味着每次加载stop_signal都会实际发生。允许编译器进行部分循环展开,如下所示:

void thread(std::atomic<bool> &stop_signal) {
    while (!stop_signal) {
        do_stuff();
    }
}
Run Code Online (Sandbox Code Playgroud)

原子load()可以观察过时的值,因此编译器可以假设四个load()s 都会读取相同的值。只需要进行一些操作,例如fetch_add()观察最新值。即便如此,这种优化也是可能的。

std::atomic实际上,任何编译器都没有实现此类优化,因此std::atomicvolatile. 这同样适用于 C 语言atomic_bool_Atomic一般类型。


1)如果至少有一个正在写入,则同一位置的两次内存访问会发生冲突,即同一位置的两次读取不会发生冲突。参见[种族介绍]

也可以看看

  • 观察到线程之间共享的*每个*值不一定需要“std::atomic”,只要存在与其相关的*某种*形式的同步,这可能很有用。例如,如果您使用“std::mutex”来保护“bool”值(可能与其他一些值一起),那么在一个线程上解锁互斥锁和在另一个线程上获取互斥锁之间的同步将导致编译器确保完成所有必要的工作,例如确保处理器缓存一致,确保读/写不会通过障碍重新排序,等等。 (7认同)

sup*_*cat 5

使用通过取消引用指针生成的左值的某些方法不会导致访问。例如,给定的int arr[5][4]; int *p;语句p = *arr;不会取消引用与 关联的任何存储arr,而只会导致编译器识别出赋值右半部分的左值是 an int[4],它将衰减为int*

除此类情况外,该标准试图将以下所有情况归类为未定义行为:该标准旨在允许实现通过随意执行访问或不执行访问来处理解引用操作,并且其决定将明显影响程序行为

在程序使用一些存储来保存 T 类型的结构,然后保存 U 类型的结构,然后再次保存 T 类型的结构,然后在不使用 T 类型的情况下复制 T 的情况下,这种哲学会导致一些相当模糊的极端情况。写入所有字段,最后用于fwrite输出 T 的整个副本。如果编译器知道原始 T 中的某个字段写入了某个值,它可能会生成将相同值存储到副本中的代码,而不考虑底层存储是否可能已更改。如果宇宙中没有任何东西会关心与该字段关联的字节在通过 处理的数据中保存的内容fwrite,那么这不应该造成问题,并且要求程序员确保与 T 关联的所有存储都被写入在将其复制为类型 T 之前使用该类型将使程序员和运行该程序的计算机都必须执行额外的无用工作。标准无法描述程序行为,这将允许实现在复制 T 时明显无法取消引用 T 的所有字段,而不将程序描述为调用未定义行为。