使用标志在线程之间进行通信

时间:2012-02-28 12:23:00

标签: c pthreads parallel-processing memory-barriers memory-fences

在互联网上,可以找到很多关于在并行编程中使用volatile关键字的争论,有时会出现矛盾的论证。

关于这个话题的一个更值得信赖的讨论似乎是this article by Arch Robison。他正在使用的示例是将值从一个线程传递到另一个线程的任务:

线程1.计算矩阵乘积并将其提供给线程2,线程2执行其他操作。矩阵是变量M,标志是volatile指针R

  
      
  1. 线程1乘法计算矩阵乘积M并进行原子设置   R指向M.
  2.   
  3. 线程2等待直到R!= NULL,然后使用M作为计算另一个矩阵乘积的因子。
  4.         

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

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

我的问题是,如何在C

中执行以下操作

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

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

4 个答案:

答案 0 :(得分:1)

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

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

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

答案 1 :(得分:1)

volatile为您提供零订购保证。在编译时(以及在弱排序ISA上的运行时),它与_Atomicmemory_order_relaxed相似。 (假设变量足够小,并且排列得足够自然,就是原子的。

当然,bool的其中只有1个字节会发生变化,因此除了01以外的其他任何事物都是不可能的。

在运行时,在有序排列的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并开始处理它,如果它愿意的话。

如果整体性能主要由大型矩阵上的操作支配,那么优化可能为时过早。