自旋锁,读写锁和顺序锁的实现原理

常用的同步原语,到多核处理器时代锁已经是必不可少的同步方式之一了。无论设计多优秀的多线程数据结构,都避不开有竞争的临界区,此时高效的锁显得至关重要。锁的颗粒度是框架/程序设计者所关注的,当然越细越好(也不尽然),同时不同的锁往往也会体现出完全不同的效率,Linux有posix的pthread_mutex_t,Windows有CreateMutex创造的HANDLE,boost有mutex而且C++11也定义了std::mutex,这些锁在获取不到锁时都会进入睡眠状态(try_lock方法例外)并将CPU让出来给其他的进程使用。但是在很多情况下,多个进程竞争的临界区的代码可能非常的短,例如:锁住数据数据锁,读取数据,修改计数器,释放临界区,再用数据计算…在这种(临界区很短的)情况下,用上面提到的那些锁会导致繁琐的用户态和内核态的切换和繁重的进程切换,这都是非常耗费计算资源的。

在临界区很短的情况下,可以考虑自旋锁作为同步原语,也因为公共数据的读取和修改是非常短暂的,但同时又是需要同步原语保护的临界区,许多所谓的无锁(lock-free)数据结构也是基于自旋锁实现的,那么什么是自旋锁呢?就是死循环检查锁是否处于未上锁状态,如果处于加锁并且退出循环,这个过程可以有xchgb这个汇编语句来保证原子性。

那么自旋锁是如何实现的呢?下面用Linux的i386架构的源码中自旋锁作为例子说明三种数据结构的实现原理,三种数据结构分别是:自旋锁(互斥锁)读写锁顺序锁

先说明xchgb这条汇编所代表的的含义:xchgb a, b,表达原子性的交换,即a复制给b,b复制给a。

自旋锁

1

2

3

typedef struct {

volatile unsigned int slock; /* 值1表示“未加锁”,任何负数和0都表示“加锁状态” */

} spinlock_t;

见上面的注释,slock值1表示“未加锁”,任何负数和0都表示“加锁状态”,volatile是类型修饰符,确保本变量相关指令不会因编译器的优化而省略,要求每次直接读值(读内存),防止编译器优化掉。旋转加锁的过程如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

/* 不阻塞的锁,无法上锁直接返回 */

inline int spin_trylock(spinlock_t *lock)

{

char oldval;

__asm__ __volatile__(

"xchgb %b0,%1"

:"=q" (oldval), "=m" (lock->slock)

:"0" (0) : "memory");

return oldval > 0;

}

/* 自旋锁,未获取锁则进入死循环,直到获取锁为止 */

inline void spin_lock(spinlock_t *lock)

{

while(!spin_trylock(lock)){

;

}

}

使用旋转锁的主要原因在于效率高,执行时间短(避免用户态和内核态之间的切换),因此Linux选择使用gcc编译器的内联汇编,__asm__ 是内联汇编的关键字,__volatile__关键字就是让gcc不要优化内存使用,如需语法见链接

读写锁

1

2

3

typedef struct {

volatile unsigned int lock;

} rwlock_t;

lock为0x01000000是标识“空”状态,如果写者已经获得自旋锁,那么lock字段的值为0x00000000,如果一个,两个或多个进程因为读而获得了自旋锁,那么lock字段上的值为0x00ffffff, 0x00fffffe等…读锁和写锁加锁如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/* 写锁尝试加锁 */

inline int _raw_read_trylock(rwlock_t *lock)

{

atomic_t *count = (atomic_t *)lock;

atomic_dec(count);

if (atomic_read(count) >= 0)

return 1;

atomic_inc(count);

return 0;

}

/* 读锁尝试加锁 */

inline int _raw_write_trylock(rwlock_t *lock)

{

atomic_t *count = (atomic_t *)lock;

/* 如果计算结果为0,则返回1,否则返回0 */

if (atomic_sub_and_test(RW_LOCK_BIAS, count))

return 1;

atomic_add(RW_LOCK_BIAS, count);

return 0;

}

顺序锁

读写锁明显有一个巨大的缺点,如果读操作非常频繁,写锁会有获取不到锁的可能,解决这个问题的办法就是使用顺序锁,但压下葫芦起了瓢,它也有自己的缺点,读者临界区的代码如下:

1

2

3

4

5

unsigned int seq;;

do{

seq = read_seqbegin(&seqlock);

/* 临界区 */

} while(read_seqretry(&reqlock, seq));

可以看出来,读者不得不反复多次读相同的数据直到获取有效的副本,但是仔细考量一下,在读操作非常大量,写操作基本没有的情况下,这个锁的效果也还是可以的。

不过还有一些限制条件:

  • 被保护的数据结构不包括间接指针指向的内容。
  • 读者临界代码区没有其他副本。

End

时间: 2024-08-05 17:03:00

自旋锁,读写锁和顺序锁的实现原理的相关文章

通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!

网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底层实现浅尝辄止,但是在需要时能够知道去查什么. 首先要打消一种想法,就是一个锁只能属于一种分类.其实并不是这样,比如一个锁可以同时是悲观锁.可重入锁.公平锁.可中断锁等等,就像一个人可以是男人.医生.健身爱好者.游戏玩家,这并不矛盾.OK,国际惯例,上干货. 〇.synchronized与Lock

写文章 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!

网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底层实现浅尝辄止,但是在需要时能够知道去查什么. 首先要打消一种想法,就是一个锁只能属于一种分类.其实并不是这样,比如一个锁可以同时是悲观锁.可重入锁.公平锁.可中断锁等等,就像一个人可以是男人.医生.健身爱好者.游戏玩家,这并不矛盾.OK,国际惯例,上干货. 〇.synchronized与Lock

自旋锁&读/写锁

自旋锁 自旋锁(spin lock)是用来在多处理器环境中工作的一种特殊的锁.如果内核控制路径发现自旋锁"开着",就获取锁并继续自己的执行.相反,如果内核控制路径发现由运行在另一个CPU上的内核控制路径"锁着",就在一直循环等待,反复执行一条紧凑的循环指令,直到锁被释放. 一般来说,由自旋锁所保护的每个临界区都是禁止内核抢占的.在单处理器系统上,这种锁本身并不起锁的作用,自旋锁原语仅仅是禁止或启用内核抢占.请注意,在自旋锁忙等期间,内核抢占还是有效的,因此,等待自旋

聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁)

读写锁是数据库中很常见的锁,又叫共享-排他锁,S锁和X锁.读写锁在大量读少量写的情况下有很高的效率优势. 读写锁是基于普通的互斥锁构建出来的更复杂的锁,它有两个基本特点: 1. 当任一线程持有读锁或写锁时,不允许其他线程再持有写锁 2. 当任一线程持有写锁时,不允许其他线程再持有读锁 也就是说,写锁是排他的,只要有一个线程持有写锁,就不允许其他线程再上锁.读锁是共享的,可以有多个线程持有读锁,但不允许同时持有写锁. 读锁和写锁还存在一个锁升级的问题,比如一个线程先持有了读锁,想升级成写锁,这时候

Go同步等待组/互斥锁/读写锁

1. 临界资源 package main import ( "fmt" "time" ) func main() { /* 临界资源: */ a := 1 go func() { a = 2 fmt.Println("goroutine中..",a) }() a = 3 time.Sleep(1) fmt.Println("main goroutine...",a) //2 } 2. 同步等待组 package main im

linux 内核的另一个自旋锁 - 读写锁

除spinlock外,linux 内核还有一个自旋锁,名为arch_rwlock_t.它的头文件是qrwlock.h,包含在spinlock.h,头文件中对它全称为"Queue read/write lock".这个锁只使用了两个成员变量就实现了读写锁.一个spinlock,以及一个整形锁变量.而spinlock就是这个Queue. 锁的原理是,当没有写意愿或写锁使用时,任意读锁可以并发.当有写意愿或写锁使用时,一切的读锁和写锁都必须进行排队. arch_rwlock_t的锁变量虽然只

zbb20180929 thread 自旋锁、阻塞锁、可重入锁、悲观锁、乐观锁、读写锁、对象锁和类锁

1.自旋锁自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行.若线程依然不能获得锁,才会被挂起.使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强.因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,往往毅然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了被挂起的操作 ,

可重入锁 公平锁 读写锁

1.可重入锁 如果锁具备可重入性,则称作为可重入锁. ========================================== (转)可重入和不可重入 2011-10-04 21:38 这种情况出现在多任务系统当中,在任务执行期间捕捉到信号并对其进行处理时,进程正在执行的指令序列就被信号处理程序临时中断.如果从信号处理程序返回,则继续执行进程断点处的正常指令序列,从重新恢复到断点重新执行的过程中,函数所依赖的环境没有发生改变,就说这个函数是可重入的,反之就是不可重入的.众所周知,在进

Java 线程锁机制 -Synchronized Lock 互斥锁 读写锁

synchronized 是互斥锁: lock 更广泛,包含了读写锁 读写锁特点: 1)多个读者可以同时进行读2)写者必须互斥(只允许一个写者写,也不能读者写者同时进行)3)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者) 互斥锁特点: 一次只能一个线程拥有互斥锁,其他线程只有等待 所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 而现在, Lock提供了比synchronize