UDN-企业互联网技术人气社区

板块导航

浏览  : 1058
回复  : 0

[运维] Linux环境多线程编程基础设施

[复制链接]
呵呵燕的头像 楼主
发表于 2016-10-6 20:18:21 | 显示全部楼层 |阅读模式
  本文介绍多线程环境下并行编程的基础设施。主要包括:

  Volatile

  __thread

  Memory Barrier

  __sync_synchronize

  volatile

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

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

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

 
  1.  volatile int i=10;//用volatile修饰变量i

  2.   ......//something happened

  3.   int b = i;//强制从内存中读取实时的i的值
复制代码


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

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

  其实我们可以这样:

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

  2.   #define ACCESS_ONCE(x) (* (volatile typeof(x) *) &(x))
复制代码


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

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

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

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

  1.   volatile int is_ready = 0;

  2.   char message[123];

  3.   void thread_A

  4.   {

  5.   while(is_ready == 0)

  6.   {

  7.   }

  8.   //use message;

  9.   }

  10.   void thread_B

  11.   {

  12.   strcpy(message,"everything seems ok");

  13.   is_ready = 1;

  14.   }
复制代码


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

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

  __thread

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

 
  1.  #include

  2.   #include

  3.   #include

  4.   using namespace std;

  5.   __thread int i = 1;

  6.   void* thread1(void* arg);

  7.   void* thread2(void* arg);

  8.   int main()

  9.   {

  10.   pthread_t pthread1;

  11.   pthread_t pthread2;

  12.   pthread_create(&pthread1, NULL, thread1, NULL);

  13.   pthread_create(&pthread2, NULL, thread2, NULL);

  14.   pthread_join(pthread1, NULL);

  15.   pthread_join(pthread2, NULL);

  16.   return 0;

  17.   }

  18.   void* thread1(void* arg)

  19.   {

  20.   cout<<++i<

  21.   return NULL;

  22.   }

  23.   void* thread2(void* arg)

  24.   {

  25.   sleep(1); //等待thread1完成更新

  26.   cout<<++i<

  27.   return NULL;

  28.   }
复制代码


  需要注意的是:

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

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

  Memory Barrier

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

  1.   int a = 1;

  2.   int b = 2;

  3.   a = b + 3;

  4.   b = 10;
复制代码


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

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

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

  生成的汇编代码可能是:

  1.   movl b(%rip), %eax ; 将b的值暂存入%eax

  2.   movl $10, b(%rip) ; b = 10

  3.   addl $3, %eax ; %eax加3

  4.   movl %eax, a(%rip) ; 将%eax也就是b+3的值写入a,即 a = b + 3
复制代码


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

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

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

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

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

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

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

  __sync_synchronize

  __sync_synchronize就是一种full barrier。

原文作者:佚名  来源:开发者头条

相关帖子

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部