澳门新蒲京娱乐


Linux进程间通信中的文件和文件锁

怎样轻松解决Word目录自动生成问题

Linux环境多线程编程基础设施,VOLATILE与内存屏障总结

  • class=”wp_keywordlink”>Volatile
  • __thread
  • Memory Barrier
  • __sync_synchronize

2.1 Acquire与Release语义

__thread

__threadgcc内置的用于多线程编程的基础设施。用__thread修饰的变量,每个线程都拥有一份实体,相互独立,互不干扰。举个例子:

#include<iostream>  
#include<pthread.h>  
#include<unistd.h>  
using namespace std;
__thread int i = 1;
void* thread1(void* arg);
void* thread2(void* arg);
int main()
{
  pthread_t pthread1;
  pthread_t pthread2;
  pthread_create(&pthread1, NULL, thread1, NULL);
  pthread_create(&pthread2, NULL, thread2, NULL);
  pthread_join(pthread1, NULL);
  pthread_join(pthread2, NULL);
  return 0;
}
void* thread1(void* arg)
{
  cout<<++i<<endl;//输出 2  
  return NULL;
}
void* thread2(void* arg)
{
  sleep(1); //等待thread1完成更新
  cout<<++i<<endl;//输出 2,而不是3
  return NULL;
}

需要注意的是:

1,__thread可以修饰全局变量、函数的静态变量,但是无法修饰函数的局部变量。

2,被__thread修饰的变量只能在编译期初始化,且只能通过常量表达式来初始化。

C++中voldatile等于插入编译器级别屏障,因此并不能阻止CPU硬件级别导致的重排。C++11
中volatile语义没有任何变化,不过提供了std::atomic工具可以真正实现原子操作,而且默认加入了内存屏障(可以通过在store与load操作时设置内存模型参数进行调整,默认为std::memory_order_seq_cst)。

本文介绍多线程环境下并行编程的基础设施。主要包括:

阻止编译器重排,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。

volatile

编译器有时候为了优化性能,会将一些变量的值缓存到寄存器中,因此如果编译器发现该变量的值没有改变的话,将从寄存器里读出该值,这样可以避免内存访问。

但是这种做法有时候会有问题。如果该变量确实(以某种很难检测的方式)被修改呢?那岂不是读到错的值?是的。在多线程情况下,问题更为突出:当某个线程对一个内存单元进行修改后,其他线程如果从寄存器里读取该变量可能读到老值,未更新的值,错误的值,不新鲜的值。

如何防止这样错误的“优化”?方法就是给变量加上volatile修饰。

volatile int i=10;//用volatile修饰变量i
......//something happened 
int b = i;//强制从内存中读取实时的i的值

OK,毕竟volatile不是完美的,它也在某种程度上限制了优化。有时候是不是有这样的需求:我要你立即实时读取数据的时候,你就访问内存,别优化;否则,你该优化还是优化你的。能做到吗?

不加volatile修饰,那么就做不到前面一点。加了volatile,后面这一方面就无从谈起,怎么办?伤脑筋。

其实我们可以这样:

int i = 2; //变量i还是不用加volatile修饰

#define ACCESS_ONCE(x) (* (volatile typeof(x) *) &(x))

需要实时读取i的值时候,就调用ACCESS_ONCE(i),否则直接使用i即可。

这个技巧,我是从《Is parallel programming hard?》上学到的。

听起来都很好?然而险象环生:volatile常被误用,很多人往往不知道或者忽略它的两个特点:在C/C++语言里,volatile不保证原子性;使用volatile不应该对它有任何Memory
Barrier
的期待。

第一点比较好理解,对于第二点,我们来看一个很经典的例子:

volatile int is_ready = 0;
char message[123];
void thread_A
{
  while(is_ready == 0)
  {
  }
  //use message;
}
void thread_B
{
  strcpy(message,"everything seems ok");
  is_ready = 1;
}

线程B中,虽然is_readyvolatile修饰,但是这里的volatile不提供任何Memory
Barrier
,因此12行和13行可能被乱序执行,is_ready = 1被执行,而message还未被正确设置,导致线程A读到错误的值。

这意味着,在多线程中使用volatile需要非常谨慎、小心。

其次具有”不可优化”性,volatile告诉编译器,不要对这个变量进行各种激进的优化,甚至将变量直接消除,保证代码中的指令一定会被执行。

Memory Barrier

为了优化,现代编译器和CPU可能会乱序执行指令。例如:

int a = 1;
int b = 2;
a = b + 3;
b = 10;

CPU乱序执行后,第4行语句和第5行语句的执行顺序可能变为先b=10然后再a=b+3

有些人可能会说,那结果不就不对了吗?b为10,a为13?可是正确结果应该是a为5啊。

哦,这里说的是语句的执行,对应的汇编指令不是简单的mov b,10和mov b,a+3。

生成的汇编代码可能是:

movl    b(%rip), %eax ; 将b的值暂存入%eax
movl    $10, b(%rip) ; b = 10
addl    $3, %eax ; %eax加3
movl    %eax, a(%rip) ; 将%eax也就是b+3的值写入a,即 a = b + 3

这并不奇怪,为了优化性能,有时候确实可以这么做。但是在多线程并行编程中,有时候乱序就会出问题。

一个最典型的例子是用锁保护临界区。如果临界区的代码被拉到加锁前或者释放锁之后执行,那么将导致不明确的结果,往往让人不开心的结果。

还有,比如随意将读数据和写数据乱序,那么本来是先读后写,变成先写后读就导致后面读到了脏的数据。因此,Memory
Barrier
就是用来防止乱序执行的。具体说来,Memory Barrier包括三种:

1,acquire barrieracquire
barrier
之后的指令不能也不会被拉到该acquire barrier之前执行。

2,release barrierrelease
barrier
之前的指令不能也不会被拉到该release barrier之后执行。

3,full barrier。以上两种的合集。

所以,很容易知道,加锁,也就是lock对应acquire
barrier
;释放锁,也就是unlock对应release
barrier
。哦,那么full barrier呢?

首先是现代编译器的代码优化和编译器指令重排可能会影响到代码的执行顺序。

__sync_synchronize

__sync_synchronize就是一种full barrier

不同编程语言中voldatile含义与实现并不完全相同,Java语言中voldatile变量可以被看作是一种轻量级的同步,因其还附带了acuire和release语义。其volatile
变量具有 synchronized
的可见性特性,但是不具备原子特性。Java语言中有volatile修饰的变量,赋值后多执行了一个“load
addl $0x0,
(%esp)”操作,这个操作相当于一个lock指令,就是增加一个完全的内存屏障指令,这点与C++实现并不一样。volatile
的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

需要注意的是其含义跟原子操作无关,比如:volatile int a; a++;
其中a++操作实际对应三条汇编指令实现”读-改-写“操作(RMW),并非原子的。

  1. 防止指令之间的重排序
  2. 保证数据的可见性

图片 1

C++11
使用std::atomic与std::atomic_thread_fence,先使用默认std::memory_order_seq_cst再进行相关优化

  • 对于Acquire来说,并没保证Acquire前的读写操作不会发生在Acquire动作之后
  • 对于Release来说,并没保证Release后的读写操作不会发生在Release动作之前

/* The “volatile” is due to gcc bugs */

当一个核心在Invalid状态进行写入时,首先会给其它CPU核发送Invalid消息,然后把当前写入的数据写入到Store
Buffer中。然后异步在某个时刻真正的写入到Cache
Line中。当前CPU核如果要读Cache Line中的数据,需要先扫描Store
Buffer之后再读取Cache Line(Store-Buffer
Forwarding)。但是此时其它CPU核是看不到当前核的Store
Buffer中的数据的,要等到Store Buffer中的数据被刷到了Cache
Line之后才会触发失效操作。

三. volatile 关键字

下面是GNU中的三种内存屏障定义方法,结合了编译器屏障和三种CPU屏障指令

#include
<atomic>std::atomic_thread_fence(std::memory_order_acquire);std::atomic_thread_fence(std::memory_order_release);

C++11提供了专门的函数,方便移植而且可以配合内存模型进行设置

相对于Synchronizes-with规则更宽松,happens-before规则定义指令执行顺序与变量的可见性,类似偏序关系,具有可传递性,因此可以运用于并行逻辑分析。

  • Strong Consistency / Sequential consistency 顺序一致性
  • Release Consistency / release-acquire / release-consume
  • Relaxed Consistency

Intel为此提供三种内存屏障指令:

X86-64一般情况根本不会需要使用lfence与sfence这两个指令,除非操作Write-Through内存或使用
non-temporal 指令(NT指令,属于SSE指令集),比如movntdq, movnti,
maskmovq,这些指令也使用Write-Through内存策略,通常使用在图形学或视频处理,Linux编程里就需要使用GNC提供的专门的函数(例子见参考资料13:Memory
part 5: What programmers can do)。

  • 变量写入操作不依赖变量当前值,或确保只有一个线程更新变量的值(Java可以,C++仍然不能)
  • 该变量不会与其他变量一起纳入
  • 变量并未被锁保护

责任编辑:

2.3 内存一致性模型 Memory Model

Acquire & Release 语义保证内存操作仅在acquire和release屏障之间发生

C++实践中推荐涉及并发问题都使用std::atomic,只有涉及特殊内存操作的时候才使用volatile关键字。这些情况通常IO相关,防止相关操作被编译器优化,也是volatile关键字发明的本意。

一. 内存屏障 Memory Barrior

指令重排中Load和Store两种操作会有Load-Store、Store-Load、Load-Load、Store-Store这四种可能的乱序结果。

2.2 happens-before规则

1.1 重排序

除此还有硬件级别Cache一致性(Cache
Coherence)带来的问题:CPU架构中传统的MESI协议中有两个行为的执行成本比较大。一个是将某个Cache
Line标记为Invalid状态,另一个是当某Cache
Line当前状态为Invalid时写入新的数据。所以CPU通过Store Buffer和Invalidate
Queue组件来降低这类操作的延时。如图:

图片 2

相关文章

No Comments, Be The First!
近期评论
    功能
    网站地图xml地图