浅谈对原子锁的理解

对原子atomic操作的理解

前言

我们知道,当我们修改某一个变量的时候,在汇编层面看来,至少需要细分为“读->改->写”三个过程,也就是说,他们访问存储单元两次,第一次读原值,第二次写新值。
假设这样一种场景,两个cpu同时对同一个存储器单元做“读->改->写”操作的话。首先,两个CPU都试图读同一个单元,但是存储器仲裁器(对访问RAM芯片的操作进行串行化的硬件电路)插手,只允许其中一个访问而不让另一个延迟,则这两个读操作会被串行化。当第一个读操作已经完成后,另一个CPU从存储器单元正好读到同一个(旧)值。然而,两个CPU都试图向那个存储器单元写一新值,总线存储器访问再一次被存储器总裁器串行化,最终,两个写操作都成功。但是,全局的结果是不对的,因为两个CPU写入同一(新)值。因此,这很容易导致意想不到的结果。

所以,避免由于“读->改->写”指令引起的竞争条件的最容易的办法,就是确保这样的操作在芯片级是原子的。任何一个这样的操作都必须以单个指令执行,中断不能中断,且避免其他的CPU访问同一存储器单元。这些很小的原子操作可以建立在其他更灵活进制的基础之上以创建临界区。
在x86平台上,总的来说,CPU提供三种独立的原子锁机制:原子
保证操作、加LOCK指令前缀和缓存一致性协议。

概念理解

原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作
(atomic operation)意为“不可被中断的一个或一系列操作”。对原子操作的简单描述就是:多个线程执行一个操作时,其中任何一个线程要么
完全执行完此操作,要么没有执行此操作的任何步骤,那么这个操作就
是原子的。原子操作是其他内核同步方法的基石。

api介绍

atomic_read(v)         返回*V
atomic_set(v, i)       把*v置成i

atomic_add(i,v)        给*v增加i
atomic_add_return(i,v) 把i加到*v,返回*V的新值

atomic_sub(i, v)       从*v中减去i 
atomic_sub_reurn(i,v)  从*v减i,返回*v的新值
atomic_sub_and_test(i,v) 从*v中减去i,如果结果为0 则返回1;否则,返回0

atomic_inc(v)            把1加到*v
atomic_dec(v)              从*v减 1
atomic_inc_return(v)     把1加到*v,返回*v新值
atomic_dec_return(v)     从*v减1,返回* V的新值

原子位操作
test_bit(nr, addr)    返回*add的第nr位的值
set_bit(nr,addr)      设置*addr的第nr位
clear_bit(nr,addr)    清*addr的第nr位
change_bit(nr, addr)     转换*addr的第nr位,并返回他的原值
  1. gcc提供的api
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);

2.dpdk提供的原子操作

static inline int rte_atomic16_cmpset(volatile uint16_t *dst, uint16_t exp, uint16_t src);
static inline uint16_t rte_atomic16_exchange(volatile uint16_t *dst, uint16_t val);
static inline void rte_atomic16_init(rte_atomic16_t *v)
static inline int16_t rte_atomic16_read(const rte_atomic16_t *v)
static inline void rte_atomic16_set(rte_atomic16_t *v, int16_t new_value)
static inline void rte_atomic16_add(rte_atomic16_t *v, int16_t inc)
static inline void rte_atomic16_sub(rte_atomic16_t *v, int16_t dec)
static inline void rte_atomic16_inc(rte_atomic16_t *v);
static inline void rte_atomic16_dec(rte_atomic16_t *v);
static inline int16_t rte_atomic16_add_return(rte_atomic16_t *v, int16_t inc)
static inline int16_t rte_atomic16_sub_return(rte_atomic16_t *v, int16_t dec)
static inline int rte_atomic16_inc_and_test(rte_atomic16_t *v);
static inline int rte_atomic16_dec_and_test(rte_atomic16_t *v);
static inline int rte_atomic16_test_and_set(rte_atomic16_t *v);
static inline void rte_atomic16_clear(rte_atomic16_t *v)

dpdk提供了16、 32和64位的原子操作API,主要实现原理是使用了**LOCK指令+CMPXCHG指令**。

对于LOCK指令前缀的总线锁,早期CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前
缀“LOCK”(这个前缀表示锁总线),经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指
令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
随着处理器的发展,对LOCK前缀的实现也在不断进行着性能改善。最近几代处理器中已经支持新的锁技术,若当前访问的内存已经被处理器缓存,LOCK#不会被触发,会用锁缓存的方式代替。这样处理原子操作的开销就在这些特定场景下进一步降低。

CMPXCHG这条指令,它的语义是比较并交换操作数(CAS,Compare And Set)。而用XCHG类的指令做内存操作,处理器会自动地遵循LOCK的语义,可见该指令是一条原子的CAS单指令操作。

源码如下

static inline int
rte_atomic64_cmpset(volatile uint64_t *dst, uint64_t exp, uint64_t src)
{
	uint8_t res;


	asm volatile(
			MPLOCKED
			"cmpxchgq %[src], %[dst];"
			"sete %[res];"
			: [res] "=a" (res),     /* output */
			  [dst] "=m" (*dst)
			: [src] "r" (src),      /* input */
			  "a" (exp),
			  "m" (*dst)
			: "memory");            /* no-clobber list */

	return res;
}

本人从dpdk移植了锁实现到自己的github里面 https://github.com/air5005/usg/tree/master/libs/liblock 有兴趣的可以参考.
3.linux kernel
内核的原子锁实现主要在x86结构里面,使用的是lock指令+内存屏障原理
提供的api基本一致,都是分为16、32、64位三种api。

  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python中的原子是一种用于多线程编程的同步机制,用于保护共享资源的访问。原子可以确保在任意时刻只有一个线程可以访问被保护的代码块,从而避免了多个线程同时修改共享资源而导致的数据竞争和不一致性。 在Python中,原子可以使用`threading`模块中的`Lock`类来实现。`Lock`类提供了两个主要方法:`acquire()`和`release()`。当一个线程调用`acquire()`方法时,如果没有被其他线程占用,则该线程会获得并继续执行;如果已经被其他线程占用,则该线程会被阻塞,直到被释放。当一个线程完成对共享资源的访问后,应该调用`release()`方法来释放,以便其他线程可以获取并访问共享资源。 下面是一个使用原子的简单示例: ```python import threading # 创建一个原子 lock = threading.Lock() # 共享资源 counter = 0 def increment(): global counter for _ in range(100000): # 获取 lock.acquire() counter += 1 # 释放 lock.release() # 创建多个线程来增加计数器的值 threads = [] for _ in range(10): t = threading.Thread(target=increment) threads.append(t) t.start() # 等待所有线程执行完毕 for t in threads: t.join() # 打印最终的计数器值 print("Counter:", counter) ``` 在上面的示例中,我们创建了一个原子`lock`,然后创建了10个线程来并发地增加计数器`counter`的值。通过使用原子,我们确保了每次只有一个线程可以访问和修改计数器的值,从而避免了数据竞争和不一致性的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值