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

本文围绕Linux原子操作展开,指出count++操作非原子性,多线程操作全局变量会有问题,常用加锁保护。介绍了__sync_fetch_and_add系列函数,有十二个原子性操作函数,能解决多线程自加问题。测试表明,不加锁不能返回正确结果,线程锁和原子性自加可返回正确结果,且__sync_fetch_and_add性能远超线程锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

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

type __sync_fetch_and_add (type *ptr, type value);
type __sync_fetch_and_sub (type *ptr, type value);
type __sync_fetch_and_or (type *ptr, type value);
type __sync_fetch_and_and (type *ptr, type value);
type __sync_fetch_and_xor (type *ptr, type value);
type __sync_fetch_and_nand (type *ptr, type value);
type __sync_add_and_fetch (type *ptr, type value);
type __sync_sub_and_fetch (type *ptr, type value);
type __sync_or_and_fetch (type *ptr, type value);
type __sync_and_and_fetch (type *ptr, type value);
type __sync_xor_and_fetch (type *ptr, type value);
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的原文,荣誉属于他,我只是学习他的代码,稍微改动了一点点。比较了两种方式的耗时情况。呵呵咱是菜鸟,不敢枉自剽窃大师作品。向大师致敬。

#define _GNU_SOURCE

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <linux/unistd.h>
#include <sys/syscall.h>
#include <errno.h>
#include<linux/types.h>
#include<time.h>

#define INC_TO 1000000 // one million...

__u64 rdtsc()
{
  __u32 lo,hi;

    __asm__ __volatile__
    (
     "rdtsc":"=a"(lo),"=d"(hi)
    );

    return (__u64)hi<<32|lo;
}

 

int global_int = 0;
pthread_mutex_t count_lock = PTHREAD_MUTEX_INITIALIZER;


pid_t gettid( void )
{
    return syscall( __NR_gettid );
}

void *thread_routine( void *arg )
{
    int i;
    int proc_num = (int)(long)arg;
    __u64 begin, end;
    struct timeval tv_begin,tv_end;
    __u64 timeinterval;
    cpu_set_t set;

    CPU_ZERO( &set );
    CPU_SET( proc_num, &set );

    if (sched_setaffinity( gettid(), sizeof( cpu_set_t ), &set ))
    {
        perror( "sched_setaffinity" );
        return NULL;
    }

    begin = rdtsc();
    gettimeofday(&tv_begin,NULL);
    for (i = 0; i < INC_TO; i++)
    {
//     global_int++;
        __sync_fetch_and_add( &global_int, 1 );
    }
    gettimeofday(&tv_end,NULL);
    end = rdtsc();
    timeinterval =(tv_end.tv_sec - tv_begin.tv_sec)*1000000                    +(tv_end.tv_usec - tv_begin.tv_usec);
    fprintf(stderr,"proc_num :%d,__sync_fetch_and_add cost             %llu CPU cycle,cost %llu us\n",                             proc_num,end-begin,timeinterval);

    return NULL;
}


void *thread_routine2( void *arg )
{
    int i;
    int proc_num = (int)(long)arg;
    __u64 begin, end;

    struct timeval tv_begin,tv_end;
    __u64 timeinterval;
    cpu_set_t set;

    CPU_ZERO( &set );
    CPU_SET( proc_num, &set );

    if (sched_setaffinity( gettid(), sizeof( cpu_set_t ), &set ))
    {
        perror( "sched_setaffinity" );
        return NULL;
    }


    begin = rdtsc();
    gettimeofday(&tv_begin,NULL);

    for(i = 0;i<INC_TO;i++)
    {
        pthread_mutex_lock(&count_lock);
        global_int++;
        pthread_mutex_unlock(&count_lock);
    }

    gettimeofday(&tv_end,NULL);
    end = rdtsc();


    timeinterval =(tv_end.tv_sec - tv_begin.tv_sec)*1000000                   +(tv_end.tv_usec - tv_begin.tv_usec);
    fprintf(stderr,"proc_num :%d,pthread lock cost %llu CPU                    cycle,cost %llu us\n",proc_num,end-begin                    ,timeinterval);

 

    return NULL;
}
int main()
{
    int procs = 0;
    int i;
    pthread_t *thrs;

    // Getting number of CPUs
    procs = (int)sysconf( _SC_NPROCESSORS_ONLN );
    if (procs < 0)
    {
        perror( "sysconf" );
        return -1;
    }

    thrs = malloc( sizeof( pthread_t ) * procs );
    if (thrs == NULL)
    {
        perror( "malloc" );
        return -1;
    }

    printf( "Starting %d threads...\n", procs );

    for (i = 0; i < procs; i++)
    {
        if (pthread_create( &thrs[i], NULL, thread_routine,
            (void *)(long)i ))
        {
            perror( "pthread_create" );
            procs = i;
            break;
        }
    }

    for (i = 0; i < procs; i++)
        pthread_join( thrs[i], NULL );

    free( thrs );

    printf( "After doing all the math, global_int value is:              %d\n", global_int );
    printf( "Expected value is: %d\n", INC_TO * procs );

    return 0;
}

测试发现:

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

### 关于 `__sync_fetch_and_add` 的使用说明 #### 函数定义 `__sync_fetch_and_add` 是 GCC 提供的一组内置原子操作函数之一,用于执行线程安全的操作。它实现了对指定内存位置的值进行原子性的获取并增加功能[^3]。 该函数的形式如下: ```c type __sync_fetch_and_add (type *ptr, type value); ``` 其中: - `ptr`: 指向目标变量的指针。 - `value`: 需要加到目标变量上的数值。 - 返回值: 原始的目标变量值(即未增加前的值)。 #### 数据类型的限制 需要注意的是,`__sync_fetch_and_add` 对数据类型有一定的约束条件。支持的数据类型仅限于整数类型及其无符号版本,具体包括 `int`, `long`, `long long` 和它们的无符号形式。如果尝试使用其他类型,则可能导致编译错误或运行时异常[^4]。 #### 编译器选项的要求 为了正确地生成针对特定处理器架构的支持指令集扩展代码,在使用这些同步原语时通常需要显式指定 `-march=` 参数给 gcc 编译器。例如,当目标机器为 i686 架构时应加入 `-march=i686` 作为编译参数。 #### 工作原理与行为描述 此函数会先读取由 ptr 所指向地址处存储的内容,并将其保存下来;接着在同一时刻对该内容加上第二个参数所给出的增量值并将更新后的结果写回同一地址之中完成整个过程而不会受到任何干扰从而保证了其原子性质。最终返回原始未经修改之前位于该位置中的旧值。 以下是具体的例子展示如何应用这一机制来实现简单的计数器逻辑: ```c #include <stdio.h> #include <stdint.h> volatile uint32_t counter; void increment_counter(void){ uint32_t old_value; do { old_value = __sync_fetch_and_add(&counter, 1); // Atomic fetch-and-add operation. } while(0); printf("Old Value:%u New Value:%u\n",old_value,(uint32_t)(old_value+1)); } int main(){ counter=0; increment_counter(); // First call increments from 0 -> prints Old Value:0 New Value:1 increment_counter(); // Second call increments from 1 -> prints Old Value:1 New Value:2 return 0; } ``` 在这个程序里展示了通过调用两次increment_counter函数分别使得初始状态下的零增长至一再进一步增至二的过程同时验证每次操作前后相应变情况均符合预期效果证明了上述方法确实能够达到目的并且具备良好的可移植性和效率表现特点。 #### 多线程环境下的优势 由于此类操作天然具有不可分割特性因此非常适合应用于并发场景当中替代传统意义上较为复杂低效的手动管理互斥量方式达成相同的目的同时还简编程模型降低了维护成本提高了整体性能水平。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术探索者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值