线程同步(2):原子操作原理

本文深入探讨了并发编程中原子操作与锁机制的区别与应用场景,通过实验证明了原子操作在多线程环境下的性能优势。文章详细介绍了原子操作的原理、函数使用方法及与锁机制的对比分析,旨在帮助开发者优化并发程序的性能。

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

1.原子操作的原理是通过锁内存总线的方式实现原子操作的

 

原子操作

假定运行在两个CPU上的两个内核控制路径试图执行非原子操作同时-修改-同一存储器单元。首先,两个CPU都试图读同一单元,但是存储器仲裁器插手,只允许其中的一个访问而让另一个延迟。然而,当第一个读操作已经完成后,延迟的CPU从那个存储器单元正好读到同一个()值。然后,两个CPU都试图向那个存储器单元写一新值,总线存储器访问再一次被存储器仲裁器串行化,最终,两个写操作都成功。但是,全局的结果是不对的,因为两个CPU写入同一()值。因此,两个交错的"-修改-"操作成了一个单独的操作。

避免由于-修改-指令引起竞争条件的最容易的办法,就是确保这样的操作在芯片级是原子的。任何一个这样的操作都必须一个单个指令执行,中间不能中断,且避免其他CPU访问同一存储单元。

在你编写C代码程序时,并不能保证编译器会为a++这样的操作使用一个原子指令。原子操作需要硬件的支持,因此是架构相关的,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。因此,Linux内核提供了一个专门的atomic_t类型和一些专门的函数和宏。原子操作主要用于实现资源计数,很多引用计数(refcnt)就是通过原子操作实现的。

原子类型定义如下:

[cpp] view plain copy

 

1. typedef struct {  

2.     volatile int counter;  

3. } atomic_t;  

volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。

Linux中的原子操作

atomic_read(v)      返回*v

atomic_set(v,i)     *v置成i

atomic_add(i,v)     *v增加i

atomic_sub(i,v)     *v减去i

atomic_sub_and_test(i,v) *v减去i,如果结果0,则返回1;否则,返回0

atomic_inc(v)       *v1

atomic_dec(v)       *v1

atomic_dec_and_test(v)   *v减去1,如果结果0,则返回1;否则,返回0

atomic_inc_and_test(v)   *v加上1,如果结果0,则返回1;否则,返回0

atomic_add_negative(i,v) i加到*v,如果结果为负,则返回1;否则,返回0

atomic_add_return(i,v)   *vi,返回*v的新值

atomic_sub_return(i,v)   *vi,返回*v的新值

[cpp] view plain copy

 

1. #ifdef CONFIG_SMP  

2. #define LOCK "lock ; "  

3. #else  

4. #define LOCK ""  

5. #endif  

6. /** 

7.  * atomic_add - add integer to atomic variable 

8.  * @i: integer value to add 

9.  * @v: pointer of type atomic_t 

10.  *  

11.  * Atomically adds @i to @v. 

12.  */  

13. static __inline__ void atomic_add(int i, atomic_t *v)  

14. {  

15.     __asm__ __volatile__(  

16.         LOCK "addl %1,%0"  

17.         :"=m" (v->counter)  

18.         :"ir" (i), "m" (v->counter));  

19. }  

在多核处理器系统中,每条指令都有一个lock字节的前缀。当控制单元检测到这个前缀时,就锁定内存总线,直到这条指令执行完为止。因此,当加锁的指令执行时,其他处理器不能访问这个内存单元。

Linux中的原子位处理函数

test_bit(nr,addr)            返回*addr的第nr位的值

set_bit(nr,addr)             设置*addr的第nr位的值

clear_bit(nr,addr)           *addr的第nr

change_bit(nr,addr)          转换*addr的第nr

test_and_set_bit(nr,addr)    设置*addr的第nr位,并返回它的原值

test_and_clear_bit(nr,addr)  *addr的第nr位,并返回它的原值

test_and_change_bit(nr,addr) 转换*addr的第nr位,并返回它的原值

atomic_clear_mask(mask,addr) 清除mask指定的*addr的所有位

atomic_set_mask(mask,addr)   设置mask指定的*addr的所有位

 

需要使用atomic_add_return函数,发现编译时找不到该函数的入口,阅读iatomic.h后发现在I386X86_64平台下,不支持带有return后缀函数,例如atomic_sub_returnatomic_inc_return等,但支持test后缀函数,如atomic_sub_and_test等。

在其它平台,如powerpcmipsarm下支持全系列的原子操作。真不知设计这个头文件的人是怎么想的。

 


/usr/include/alsa/iatomic.h,也能实现原子操作,使用的时候#include<alsa/iatomic.h>就可以了,原有的atomic系列函数这里都有,包括atomic_readatomic_setatomic_incatomic_addatomic_sub,但是该函数在RedHat(RHEL)6.0以上版本也被取消了。

只支持32位数据的原子操作。

-------------------------------正文---------------------------------------

     Linux2.6.18之后,删除了<asm/atomic.h><asm/bitops.h>GCC提供了内置的原子操作函数,更适合用户态的程序使用。现在atomic.h在内核头文件中,不在gcc默认搜索路径下,即使像下面这样强行指定路径,还是会出现编译错误。

 

[cpp] view plain copy print?

#include</usr/src/linux-headers-2.6.27-7/include/asm-x86/atomic.h>  

gcc4.1.0提供了__sync_*系列的built-in函数,用于提供加减和逻辑运算的原子操作,我是传送门

可以对1,2,48字节长度的数值类型或指针进行原子操作,其声明如下

[cpp] view plain copy print?

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, ...)  

          { tmp = *ptr; *ptr op= value; return tmp; }  

          { tmp = *ptr; *ptr = ~tmp & value; return tmp; }   // nand  

  

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, ...)  

          { *ptr op= value; return *ptr; }  

          { *ptr = ~*ptr & value; return *ptr; }   // nand  

这两组函数的区别在于第一组返回更新前的值,第二组返回更新后的值,下面的示例引自这里

 

[cpp] view plain copy print?

#include <stdio.h>  

#include <pthread.h>  

#include <stdlib.h>  

  

static int count = 0;  

  

void *test_func(void *arg)  

{  

        int i=0;  

        for(i=0;i<20000;++i){  

                __sync_fetch_and_add(&count,1);  

        }  

        return NULL;  

}  

  

int main(int argc, const char *argv[])  

{  

        pthread_t id[20];  

        int i = 0;  

  

        for(i=0;i<20;++i){  

                pthread_create(&id[i],NULL,test_func,NULL);  

        }  

  

        for(i=0;i<20;++i){  

                pthread_join(id[i],NULL);  

        }  

  

        printf("%d/n",count);  

        return 0;  

}  

对于使用atomic.h的老代码,可以通过宏定义的方式,移植到高内核版本的linux系统上,例如

[cpp] view plain copy print?

#define atomic_inc(x) __sync_add_and_fetch((x),1)  

#define atomic_dec(x) __sync_sub_and_fetch((x),1)  

#define atomic_add(x,y) __sync_add_and_fetch((x),(y))  

#define atomic_sub(x,y) __sync_sub_and_fetch((x),(y))  

 

  当然我们知道,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);

// sam:很纳闷为什么后边要写省略号,是不是还有不需要我们关心的参数?用的时候不需要传参数?下面这两个函数正是哥想要的啦,可以轻松实现互斥锁的功能。

bool __sync_bool_compare_and_swap (type*ptr, type oldval, type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval,  type newval, ...)
这两个函数提供原子的比较和交换,如果*ptr == oldval,就将newval写入*ptr,
第一个函数在相等并写入的情况下返回true.
第二个函数在返回操作之前的值。
__sync_synchronize (...)

理解上面这个东西,参照:http://blog.sunchangming.com/post/47188394133
还有两个函数:
type __sync_lock_test_and_set (type *ptr, type value, ...)
*ptr设为value并返回*ptr操作之前的值。
void __sync_lock_release (type *ptr, ...)
*ptr0

 


    需要提及的是,这个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;
}
     通过我的测试发现:

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
1 不加锁的情况下,不能返回正确的结果
  测试程序结果显示,正确结果为400万,实际为1169911.

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值