在互联网上,可以找到很多关于在并行编程中使用volatile
关键字的争论,有时会出现矛盾的论证。
关于这个话题的一个更值得信赖的讨论似乎是this article by Arch Robison。他正在使用的示例是将值从一个线程传递到另一个线程的任务:
线程1.计算矩阵乘积并将其提供给线程2,线程2执行其他操作。矩阵是变量M
,标志是volatile
指针R
。
- 线程1乘法计算矩阵乘积M并进行原子设置 R指向M.
- 线程2等待直到R!= NULL,然后使用M作为计算另一个矩阵乘积的因子。
醇>换句话说,M是一条消息,R是就绪标志。
作者声称,虽然将R声明为volatile将解决将更改从线程1传播到线程2的问题,但它不能保证M发生这种情况时M的值是什么。并且R
和M
的分配可以重新排序。因此,我们需要使M
和R
兼容,或者在某些库中使用某些同步机制,例如pthreads。
我的问题是,如何在C
中执行以下操作1)如何在两个线程之间共享一个标志 - 如何以原子方式分配给它,确保另一个线程将看到更改并测试另一个线程中的更改。在这种情况下使用volatile合法吗?或者某些图书馆可以提供概念上更好或更快的方式,可能涉及内存障碍?
2)如何正确执行Robison的示例,以及如何将矩阵M从一个线程发送到另一个线程并安全地执行(并且最好使用pthreads进行移植)
答案 0 :(得分:1)
在像x86这样的体系结构中,默认情况下,像指针一样正确对齐(和大小)的变量将以原子方式读取和写入,但需要发生的是内存读/写的序列化以防止在CPU管道中重新排序(通过使用显式内存栅栏或总线锁定操作)以及使用volatile
来阻止编译器重新排序它生成的代码。
最简单的方法是使用CAS。大多数CAS内在函数在编译器和CPU内存总线级别提供完整的内存屏障。在MSVC下,您可以使用Interlock*
函数,BTS,BTR,Inc,Dec,Exchange和Add都可以用于标记,对于GCC,您可以使用基于__sync_*
的变体。
对于更多便携式选项,您可以使用pthread_mutex
或pthread_cond
。如果您可以使用C11,还可以查看_Atomic
关键字。
答案 1 :(得分:1)
volatile
为您提供零订购保证。在编译时(以及在弱排序ISA上的运行时),它与_Atomic
和memory_order_relaxed
相似。 (假设变量足够小,并且排列得足够自然,就是原子的。
当然,bool
的其中只有1个字节会发生变化,因此除了0
或1
以外的其他任何事物都是不可能的。
在运行时,在有序排列的x86上,asm加载/存储具有acq / rel排序,因此,如果volatile
碰巧不进行重新排序,则该构建是“安全的”。
When to use volatile with multi threading?(绝对不要:如果需要的话,将原子与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
}
答案 2 :(得分:0)
“volatile”是编译器不优化存储器访问的提示,即,不假设自上次(本地)写入以来存储器中的值未改变。如果没有这个提示,编译器可以假定从中复制变量的寄存器的值仍然有效。 因此,虽然矩阵不太可能保留在寄存器内,但通常两个变量都应该是易失性的,或者对接收器来说更加精确,易变。
在现实生活多线程中,人们宁愿使用信号量或类似的信号,避免忙于等待接收器。
答案 3 :(得分:0)
“经典”方法是让线程1将指向动态分配的矩阵的指针推送到线程2等待的生产者 - 消费者队列上。一旦推送,线程1可以分配另一个M并开始处理它,如果它愿意的话。
如果整体性能主要由大型矩阵上的操作支配,那么优化可能为时过早。