原子变量的原理与应用

当多个线程并发执行时,由于CPU随时被抢占,程序的执行就会具有不可预测性。CPU还需要处理各种外围设备的中断。这种不可预测性会变的更随机。我们所期望的程序执行结果有可能会变得不可预期。
举例说明:考虑生产者消费者的问题,我们有一个变量counter,生产者增加conter的值,消费者减少counter的值。如下所示:
1
/*生产者操作P*/
2
…………
3
register1 = counter;
4
register1 = register1+1;
5
counter = register1
1
/*消费者操作C*/
2
…………
3
register2 = counter;
4
register2 = register2-1;
5
counter = register2
6
……

假如这段操作的,counter的初始值为5, 那么最终的值可能是4、5、6.下表中是其中一次的执行情况。我们可以采用此表演示出其他情况来。
线程 执行语句 结果
P register1 = counter; register1 = 5
C register2 = counter; register2 = 5
P register1 = register1+1; register1 = 6
P counter = register1 counter = 6
C register2 = register2-1; register2 = 4
C counter = register2 counter = 4

为此,系统层面上需要提供一定的同步和互斥方式以解决上述问题。 在Linux内核中,系统提供了多种方式的多线程的互斥方式,包括信号量(semaphores)、互斥锁(Mutexes)、完成量(completions)、自旋锁(spinlocks)、原子量(atomic Variables)等,以上各种机制适合于不同的场景。
原子量是一种轻量级的互斥操作,他的实现原理是确保指定的变量在进行操作时,不能被访问。因此,如果我们程序中所使用的互斥的资源只是一个变量时,可以采用原子量确保系统的互斥。比如,我们需要一个量来记录在有多少个进程访问某个文件时,就可以采用一个原子变量。

Linux内核提供了现成的原子变量API可供使用:
接口 说明
void atomic_set(atomic_t *v, int i); 将i的值赋值给变量v
int atomic_read(atomic_t *v); 返回变量v当前的值
void atomic_add(int i, atomic_t *v); 将i的值累加到原子变量v中
void atomic_sub(int i, atomic_t *v); 原子变量v中减掉i的值
void atomic_inc(atomic_t *v); 将变量v的值累加1
void atomic_dec(atomic_t *v); 将变量v的值减1
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
分析进行累加、减1、减少某个值后,判断其值是否为0
int atomic_add_negative(int i, atomic_t *v); 累加之后判断其值是否为负。如果为不为负的话返回0
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
进行相应的操作之后,然后再返回最终保存的值

在用户空间,gcc从4.1.2提供了__sync_*系列的built-in函数,用于提供加减和逻辑运算的原子操作。
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, ...)
除此之外的,我们也可以将内核中实现的原子操作函数移植到用户空间使用,当然也可重复造轮子,自己写一套。

在软件设计时,使用原子变量需要考虑从临界区的问题。原子操作是将程序的临界区缩小到一个调用之内。可以理解为,对原子变量进行操作时,CPU之间是访问是互斥的。原子变量上下的代码都不在临界区之外,因此原子操作上下的代码段都不能保证其操作过程,没有其他进程在执行与之相关联的操作。这里的相关联指的是,对当前的代码段结执行结果产生影响。
与此同时,在使用原子变量时,还要确保我们所定义的原子变量不被除上述所提供的原子变量操作函数之外的函数进行操作。

在实现的角度,上述的操作是与平台相关的。以X86平台为例,CPU从三个方面确保上述操作的原子性。
(1)确保相关操作在一个CPU执行上是原子的,即操作不会被中断,或者说访存期间不会被打断。所述的操作包括字节、按16位对齐的字(Word)、按32位对齐的双字;新架构的CPU可以支持不对齐的访问,但对性能有影响。
(2)在执行某些操作时,可以显性的进行锁前端总线,以使其他CPU不能够进行访存操作。
(3) 从P4以后的CPU,当显性加锁时,缓存一致性协议能够保证在缓存中执行的操作,不被其他CPU访问。这个操作进一步缩小了加锁的范围。使原来其他CPU不能够访问整个内存缩小到不充许访问执行相关指令的CPU缓存相关联的内存。
当然为了确保系统的性能并且确保锁机制不被随意使用,CPU设计上还对加锁的指令做了一定的限制,能够显性加锁的指令是有限的,如BTS、BTR、BTC、XADD、 CMPXCHG等等。如果在使用非限定范围的指令会造成CPU抛出异常。
有了CPU的支持,我们实现多CPU间的原子操作就变的比较简单了。只要在执行相应的指令之间加锁就可以。
下面是原子加(atomic_add)实现的函数:
1
static inline void atomic_add(int i,atomic *v)
2
{
3
    __asm__ __volatile__("lock ; "addl %1,%0":"=m"(v->counter):"ir"(i),"m"(v->counter));
4
}
最后,需要说明的是,除了原子操作之外,其他操作,比如自旋锁、信号量都需要通过原子操作保存相关的信息。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值