2

在 Internet 上,可以找到许多关于volatile在并行编程中使用关键字的争论,有时争论不休。

关于这个主题的更值得信赖的讨论之一似乎是Arch Robison 的这篇文章。他使用的示例是将值从一个线程传递到另一个线程的任务:

线程 1. 计算一个矩阵乘积并将其提供给线程 2,线程 2 用它做其他事情。矩阵是可变M的,标志是volatile指针R

  1. 线程 1 乘以计算矩阵乘积 M 并以原子方式将 R 设置为指向 M。
  2. 线程 2 等到 R!=NULL 然后使用 M 作为一个因子来计算另一个矩阵乘积。

换句话说,M 是一条消息,R 是一个就绪标志。

作者声称,虽然将 R 声明为 volatile 将解决将更改从线程 1 传播到线程 2 的问题,但它不能保证发生这种情况时 M 的值是多少。分配给RM可以重新排序。因此,我们需要在 pthreads 之类的库中同时使用MRvolatile 或使用一些同步机制。

我的问题是,如何在 C 中执行以下操作

1)如何在两个线程之间共享一个标志 - 如何原子地分配给它,确保另一个线程将看到更改并测试另一个线程中的更改。在这种情况下使用 volatile 合法吗?或者某些库能否提供概念上更好或更快的方法,可能涉及内存屏障?

2)如何正确地执行 Robison 的示例,以及如何将矩阵 M 从一个线程发送到另一个线程并安全地进行(最好使用 pthreads 便携)

4

4 回答 4

1

在像 x86 这样的架构下,默认情况下,像指针这样正确对齐(和大小)的变量将被原子地读取和写入,但需要发生的是内存读/写的序列化,以防止在 CPU 管道中重新排序(通过使用显式内存栅栏或总线锁定操作)以及volatile用于防止编译器重新排序它生成的代码。

最简单的方法是使用 CAS。大多数 CAS 内在函数在编译器和 CPU 内存总线级别提供完整的内存屏障。在 MSVC 下,您可以使用Interlock*功能,BTS、BTR、Inc、Dec、Exchange 和 Add 都适用于标志,对于 GCC,您将使用__sync_*基于的变体。

对于更便携的选项,您可以使用 apthread_mutexpthread_cond. 如果您可以使用C11,您还可以查看_Atomic关键字。

于 2012-02-28T12:56:55.880 回答
1

“volatile”提示编译器不要优化内存访问,即不要假设内存中的值自上次(本地)写入以来没有改变。如果没有此提示,编译器可能会假定从中复制变量的寄存器的值仍然有效。因此,虽然矩阵不太可能保存在寄存器中,但通常这两个变量都应该是易失的,或者更准确地说,对于接收者来说是易失的。

在现实生活中的多线程中,人们宁愿使用信号量或类似的东西来发送信号,避免忙于等待接收器。

于 2012-02-28T12:41:16.623 回答
1

volatile为您提供零订购保证。在编译时(以及弱序 ISA 上的运行时),它类似于_Atomicwith memory_order_relaxed。(假设变量足够小并且对齐足够自然是原子的。

当然,它bool只有 1 个字节会发生变化,所以除了0或之外的任何东西1都是不可能的。

在强排序 x86 上运行时,asm 加载/存储具有 acq/rel 排序,所以如果volatile碰巧没有重新排序,那么它对于该构建是“安全的”。

何时在多线程中使用 volatile? (从不:如果你想要的话,可以使用 atomic 和 memory_order_relaxed。)


对于“数据就绪”标志,您实际上需要释放/获取语义。 https://preshing.com/20120913/acquire-and-release-semantics/

如何在两个线程之间共享一个标志 - 如何以原子方式分配给它,确保另一个线程将看到更改并测试另一个线程中的更改。

#include <stdatomic.h>
// shared:
_Atomic bool data_ready = false;
float shared_matrix[N][N];

在生产者中:

   write_matrix( &shared_matrix );  // loop that fills a buffer
   atomic_store_explicit(&data_ready, true, memory_order_release);
   // data_ready = true  but with only release, not seq_cst for efficiency

在消费者中:

#include <immintrin.h>   // ifdef __x86__

void consumer() {
   while(!atomic_load_explicit(&data_ready, memory_order_acquire)) {
       _mm_pause();   // for x86 spin loops
   }
   // now safe to read matrix
}
于 2020-01-29T00:56:08.180 回答
0

“经典”方式是线程 1 将指向动态分配矩阵的指针推送到线程 2 正在等待的生产者-消费者队列中。一旦推送,线程 1 可以分配另一个 M 并开始处理它,如果它愿意的话。

如果整体性能由大型矩阵上的操作主导,那么摆弄 volatile 标志等作为优化可能还为时过早。

于 2012-02-28T13:00:22.370 回答