• Linux:无锁化编程 __sync_fetch_and_add原理及其实现分析


    背景

    linux支持的哪些操作是具有原子特性的?知道这些东西是理解和设计无锁化编程算法的基础。

    我们知道,count++操作不是原子的。一个自加操作,本质是分成三步的:

    1.      从缓存取到寄存器
    2.      在寄存器加1
    3.      存入缓存。

        由于时序的因素,多个线程操作同一个全局变量,会出现问题。这也是并发编程的难点。在目前多核条件下,这种困境会越来越彰显出来。
        最简单的处理办法就是加锁保护,看下面的代码:
     

    pthread_mutex_t count_lock = PTHREAD_MUTEX_INITIALIZER;

    pthread_mutex_lock(&count_lock);
    global_int++;
    pthread_mutex_unlock(&count_lock);

     原子操作函数       

            在网上查找资料,找到了__sync_fetch_and_add系列的命令,发现这个系列命令讲的最好的一篇文章,英文好的同学可以直接去看原文。Multithreaded simple data type access and atomic variables。

            

            __sync_fetch_and_add系列一共有十二个函数,有加/减/与/或/异或/等函数的原子性操作函数,__sync_fetch_and_add,顾名思义,先fetch,然后自加,返回的是自加以前的值。以count = 4为例,调用__sync_fetch_and_add(&count,1),之后,返回值是4,然后,count变成了5.
         有__sync_fetch_and_add,自然也就有__sync_add_and_fetch,这个的意思就很清楚了,先自加,再返回。他们两者得关系与i++和++i的关系是一样的。
       

            有了这个函数,我们就有新的解决办法了。对于多线程对全局变量进行自加,我们就再也不用理线程锁了。下面这行代码,和上面被pthread_mutex保护的那行代码作用是一样的,而且也是线程安全的。

    __sync_fetch_and_add( &global_int, 1 );


        下面是这群函数的全家福,大家看名字就知道是这些函数是干啥的了。

        在用gcc编译的时候要加上选项 -march=i686

    1. type __sync_fetch_and_add (type *ptr, type value);
    2. type __sync_fetch_and_sub (type *ptr, type value);
    3. type __sync_fetch_and_or (type *ptr, type value);
    4. type __sync_fetch_and_and (type *ptr, type value);
    5. type __sync_fetch_and_xor (type *ptr, type value);
    6. type __sync_fetch_and_nand (type *ptr, type value);
    7. type __sync_add_and_fetch (type *ptr, type value);
    8. type __sync_sub_and_fetch (type *ptr, type value);
    9. type __sync_or_and_fetch (type *ptr, type value);
    10. type __sync_and_and_fetch (type *ptr, type value);
    11. type __sync_xor_and_fetch (type *ptr, type value);
    12. type __sync_nand_and_fetch (type *ptr, type value);

            

            要提及的是,这个type不能够瞎搞。

            下面看下__sync_fetch_and_add反汇编出来的指令:

    804889d: f0 83 05 50 a0 04 08 lock addl $0x1,0x804a050

        我们看到了,addl前面有个lock,这行汇编指令码前面是f0开头,f0叫做指令前缀,Richard Blum将指令前缀分成了四类,有兴趣的同学可以看下。其实我也没看懂,intel的指令集太厚了,没空看。总之解释了,lock前缀的意思是对内存区域的排他性访问。
    ? Lock and repeat prefixes
    ? Segment override and branch hint prefixes
    ? Operand size override prefix
    ? Address size override prefix

         前文提到,lock是锁FSB,前端串行总线,front serial bus,这个FSB是处理器和RAM之间的总线,锁住了它,就能阻止其他处理器或者core从RAM获取数据。当然这种操作是比较费的,只能操作小的内存可以这样做,想想我们有memcpy ,如果操作一大片内存,锁内存,那么代价就太昂贵了。所以前文提到的_sync_fetch_add_add家族,type只能是int long  ,long long(及对应unsigned类型)。
     

            下面提供了函数,是改造的Alexander Sundler的原文,荣誉属于他,我只是学习他的代码,稍微改动了一点点。比较了两种方式的耗时情况。呵呵咱是菜鸟,不敢枉自剽窃大师作品。向大师致敬。

    1. #define _GNU_SOURCE
    2. #include <stdio.h>
    3. #include <pthread.h>
    4. #include <unistd.h>
    5. #include <stdlib.h>
    6. #include <sched.h>
    7. #include <linux/unistd.h>
    8. #include <sys/syscall.h>
    9. #include <errno.h>
    10. #include<linux/types.h>
    11. #include<time.h>
    12. #define INC_TO 1000000 // one million...
    13. __u64 rdtsc()
    14. {
    15. __u32 lo,hi;
    16. __asm__ __volatile__
    17. (
    18. "rdtsc":"=a"(lo),"=d"(hi)
    19. );
    20. return (__u64)hi<<32|lo;
    21. }
    22. int global_int = 0;
    23. pthread_mutex_t count_lock = PTHREAD_MUTEX_INITIALIZER;
    24. pid_t gettid( void )
    25. {
    26. return syscall( __NR_gettid );
    27. }
    28. void *thread_routine( void *arg )
    29. {
    30. int i;
    31. int proc_num = (int)(long)arg;
    32. __u64 begin, end;
    33. struct timeval tv_begin,tv_end;
    34. __u64 timeinterval;
    35. cpu_set_t set;
    36. CPU_ZERO( &set );
    37. CPU_SET( proc_num, &set );
    38. if (sched_setaffinity( gettid(), sizeof( cpu_set_t ), &set ))
    39. {
    40. perror( "sched_setaffinity" );
    41. return NULL;
    42. }
    43. begin = rdtsc();
    44. gettimeofday(&tv_begin,NULL);
    45. for (i = 0; i < INC_TO; i++)
    46. {
    47. // global_int++;
    48. __sync_fetch_and_add( &global_int, 1 );
    49. }
    50. gettimeofday(&tv_end,NULL);
    51. end = rdtsc();
    52. timeinterval =(tv_end.tv_sec - tv_begin.tv_sec)*1000000 +(tv_end.tv_usec - tv_begin.tv_usec);
    53. fprintf(stderr,"proc_num :%d,__sync_fetch_and_add cost %llu CPU cycle,cost %llu us\n", proc_num,end-begin,timeinterval);
    54. return NULL;
    55. }
    56. void *thread_routine2( void *arg )
    57. {
    58. int i;
    59. int proc_num = (int)(long)arg;
    60. __u64 begin, end;
    61. struct timeval tv_begin,tv_end;
    62. __u64 timeinterval;
    63. cpu_set_t set;
    64. CPU_ZERO( &set );
    65. CPU_SET( proc_num, &set );
    66. if (sched_setaffinity( gettid(), sizeof( cpu_set_t ), &set ))
    67. {
    68. perror( "sched_setaffinity" );
    69. return NULL;
    70. }
    71. begin = rdtsc();
    72. gettimeofday(&tv_begin,NULL);
    73. for(i = 0;i<INC_TO;i++)
    74. {
    75. pthread_mutex_lock(&count_lock);
    76. global_int++;
    77. pthread_mutex_unlock(&count_lock);
    78. }
    79. gettimeofday(&tv_end,NULL);
    80. end = rdtsc();
    81. timeinterval =(tv_end.tv_sec - tv_begin.tv_sec)*1000000 +(tv_end.tv_usec - tv_begin.tv_usec);
    82. fprintf(stderr,"proc_num :%d,pthread lock cost %llu CPU cycle,cost %llu us\n",proc_num,end-begin ,timeinterval);
    83. return NULL;
    84. }
    85. int main()
    86. {
    87. int procs = 0;
    88. int i;
    89. pthread_t *thrs;
    90. // Getting number of CPUs
    91. procs = (int)sysconf( _SC_NPROCESSORS_ONLN );
    92. if (procs < 0)
    93. {
    94. perror( "sysconf" );
    95. return -1;
    96. }
    97. thrs = malloc( sizeof( pthread_t ) * procs );
    98. if (thrs == NULL)
    99. {
    100. perror( "malloc" );
    101. return -1;
    102. }
    103. printf( "Starting %d threads...\n", procs );
    104. for (i = 0; i < procs; i++)
    105. {
    106. if (pthread_create( &thrs[i], NULL, thread_routine,
    107. (void *)(long)i ))
    108. {
    109. perror( "pthread_create" );
    110. procs = i;
    111. break;
    112. }
    113. }
    114. for (i = 0; i < procs; i++)
    115. pthread_join( thrs[i], NULL );
    116. free( thrs );
    117. printf( "After doing all the math, global_int value is: %d\n", global_int );
    118. printf( "Expected value is: %d\n", INC_TO * procs );
    119. return 0;
    120. }

    测试发现:

    1 不加锁的情况下,不能返回正确的结果
      测试程序结果显示,正确结果为400万,实际为1169911.

    2 线程锁和原子性自加都能返回正确的结果。

    3 性能上__sync_fetch_and_add,完爆线程锁。
      从测试结果上看, __sync_fetch_and_add,速度是线程锁的6~7倍
     

    且看以下测试结果:

    Starting 4 threads...
    proc_num :2,no locker cost 27049544 CPU cycle,cost 12712 us
    proc_num :0,no locker cost 27506750 CPU cycle,cost 12120 us
    proc_num :1,no locker cost 28499000 CPU cycle,cost 13365 us
    proc_num :3,no locker cost 27193093 CPU cycle,cost 12780 us
    After doing all the math, global_int value is: 1169911
    Expected value is: 4000000
    Starting 4 threads...
    proc_num :2,__sync_fetch_and_add cost 156602056 CPU cycle,cost 73603 us
    proc_num :1,__sync_fetch_and_add cost 158414764 CPU cycle,cost 74456 us
    proc_num :3,__sync_fetch_and_add cost 159065888 CPU cycle,cost 74763 us
    proc_num :0,__sync_fetch_and_add cost 162621399 CPU cycle,cost 76426 us
    After doing all the math, global_int value is: 4000000
    Expected value is: 4000000

    Starting 4 threads...
    proc_num :1,pthread lock cost 992586450 CPU cycle,cost 466518 us
    proc_num :3,pthread lock cost 1008482114 CPU cycle,cost 473998 us
    proc_num :0,pthread lock cost 1018798886 CPU cycle,cost 478840 us
    proc_num :2,pthread lock cost 1019083986 CPU cycle,cost 478980 us
    After doing all the math, global_int value is: 4000000
    Expected value is: 4000000

  • 相关阅读:
    python中使用pytest框架集成allure测试报告
    Spring中Bean的作用域
    java毕业设计软件基于SpringBoot在线电影订票|影院购票系统
    ES6 Class(类) 总结(九)
    Linux SSH免密登录
    刷题日记【第十一篇】-笔试必刷题【小易的升级之路+找出字符串中第一个只出现一次的字符+微信红包+计算字符串的编辑距离】
    二:对表进行基本CRUD操作
    blob相关
    Redis快速入门----------客户端使用
    免费学习Linux!
  • 原文地址:https://blog.csdn.net/hhd1988/article/details/127545400