- 浏览: 79421 次
- 性别:
- 来自: 北京
文章分类
最新评论
引用:http://www.ibm.com/developerworks/cn/linux/l-cn-rwspinlock3/
本系列文章的第 2 部分中给出的实现都基于简单共享变量,简洁实用,但在大规模多核、NUMA 系统上可扩展性较差。我们说某个读写自旋锁的实现是可扩展的,通俗地讲是指在线程访问模式(读者写者数目之比、各自到来的频率及持有锁的时间)不变的前提下增加处理器的个数,线程的吞吐量(单位时间内获得锁的线程数目)也随之大幅增加。如果能接近线性(甚至由于缓存的影响使得超线性)增加,我们说该实现是高可扩展的。
基于简单共享变量的读写自旋锁的可扩展性较差本质上是因为操作共享变量的代价过大:如果系统不能保证缓存的一致性,读写共享变量导致总线的流量激增;即使提供了一致性的缓存,频繁的写操作也会增大处理器间的缓存同步开销。具体而言有 3 点:
- 所有等待线程均在某个(些)共享变量上自旋。
- 线程需要通过共享变量来分享一些信息,比如需要一个 nr_readers 的整型变量记录同时持有锁的读者。
- 这些共享变量频繁被修改。
因此我们可以针对上述 3 点,分别提出相应的改进策略:
- 所有等待线程均在局部变量上自旋。
- 尽量减少共享变量的数目。
- 如果必须使用共享变量,那么考虑使用该变量的可扩展实现。
首先我们考虑如何让等待线程只在局部变量上自旋,这涉及四个问题:
- 线程到来时是否需要自旋?如果锁被写者持有或尚有有等待线程,显然新来的线程只能选择自旋等待;如果锁无人持有,线程无需自旋;如果只有读者持有且新来的线程也是读者,那么为了提高并发性,新来的读者最好不用等待。
- 谁负责通知某个等待线程结束自旋?这个问题比较容易回答,应该由持有锁的线程负责通知。
- 何时通知?持有锁的写者必须在释放锁的时候才能通知下一个候选持有者:而读者在持获得锁后应该检查一下下一个候选者是否也是读者,若是,则立即通知。
- 如何通知?这个问题要求我们能够拥有某种能力可以找到下一个候选持有者,即需要使用特殊的数据结构。
我们很容易想到可以用单向链表将申请线程组织成一个队列,每个线程能够通过指针寻找到自己的所有后继,而且线程申请锁的顺序与线程在队列中的位置保持一致,因此用单向链表可以实现一个公平的读写自旋锁 [3]。
凭空想象算法似乎有些困难,我们还是先在草稿纸上画一个示意图,有了感性认识之后再考虑数据结构和算法的细节。
图 1. 基于单向链表的公平读写自旋锁示意图
从图上可以看到,为了构建单向链表,每个线程必须有一个自己私有的链表节点数据结构,里面至少包含三个域:自身角色 type、自旋变量 waiting 和指向直接后继的 next 指针。我们还需要一个指向链表尾部的指针 tail,新到的线程通过它可以获得其直接前驱,从而将自己插入链表。这个 tail 指针应当放到锁的数据结构中。
值得注意的是,插入链表这个动作并不是原子的,至少需要 2 个操作:原子地获得 tail 指针并将其指向自己;将直接前驱的 next 指针指向自己。但是线程的直接前驱随时可能释放锁,并继续申请锁而重用其数据结构,因此需要约束线程的行为以保证单向链表的一致性。我们规定:线程释放锁之前必须检查自身是否存在直接后继线程,如果存在则保证在直接后继插入链表之后才能释放锁。这可以通过检查 tail 和 next 指针实现。
连续的读者可以同时持有锁,但是它们并不一定按照申请的顺序释放锁,故而难以在释放锁的时候继续保持这些读者间的链表结构。这是因为我们使用的是单向链表,释放锁的读者很难用简单高效的方法将其节点数据结构从链表中摘下 [4](即使保存了直接前驱的指针,也可能已经失效),而且该读者也许需要继续申请锁而重用该数据结构,导致其尚未释放锁的直接前驱读者也不能再使用 next 指针遍历后继。单向链表的结构被破坏带来如下 2 个挑战:
- 从图 2 可以看到,如果 A1– A3都释放锁的时候,应该通知 A4这个写者。但是 A1– A3如何知道自己是最后一个持有者?
- 如果 A2知道自己最后释放锁,但是 A4并不是它的直接后继,A2如何通知 A4呢?
一种直观简单的解决方案是用一个计数器 nr_readers 记录当前同时持有锁的数目,读者获得锁前原子递增 nr_readers;释放锁的时候原子递减,如果递减后 nr_readers 为 0,表示自己此刻是最后一个持有锁的读者。此外还需要一个记录等待线程中的第一个写者的指针 next_writer。这两个变量也应当放到锁的数据结构中。
读者并发还带来一个棘手的问题。还是以图 2 为例,当读者 A1获得锁时,如果此时知道 A2已经到来了(通过检查自己的 next 指针),那么应该由 A1通知 A2。但是可能 A2到来的时候 A1已经在访问共享资源或者正在释放锁,那么 A2应该自行获得锁。因为获得锁涉及到原子递增 nr_readers,所以 A1和 A2必须达成一致。我们的方案是:读者到来时先检查其直接前驱读者是否还在自旋,若是,则原子地在其前驱的节点数据结构中标记一个特殊值。如果标记成功,表明前驱尚未结束自旋,那么由前驱负责通知;如果标记失败,说明前驱已经退出自旋而获得锁,那么读者直接获得锁。标记值可以选得有意义一些,这儿我们选择线程的角色,于是线程很容易知道直接后继的类型,而不用通过 next 指针进一步查看。我们在节点数据结构中增加变量 successor_type。
下面我们详细阐述读写自旋锁的算法。假设申请线程为 A。
A 是读者,申请锁:
- A 使用原子交换操作将读写自旋锁的 tail 指针指向自己的 qnode 结构以确定在链表中的位置,并返回原来的 tail 值作为自己的直接前驱 pred。即使多个线程同时申请锁,由于交换操作的原子性,每个执行线程的申请顺序将会被唯一确定,不会出现不一致的现象。
- 如果 pred 等于 NULL,说明锁无人持有或只有读者持有(A 的直接前驱是读者,且已经释放锁),那么 A 可以立即持有锁,跳到第 4 步。
- 此时 pred 不等于 NULL。如果 pred 是写者,A 只能依赖 pred 在释放锁的时候通知自己,所以 A 所要做的只是将 pred->next 指向自己的,然后自旋等待。如果 pred 是读者,那么需要按照前面描述的那样,向 pred 标记 A 的读者角色。如果标记成功,则取得锁,跳到第 4 步;否则自旋等待。
- A 准备结束函数调用,但还需要检查一下自己的 successor_type 变量,如果直接后继是读者,那么先等它将链表构建完整,然后通知其结束自旋。
A 是读者,释放锁:
- A 先检查是否存在直接后继。这可以用原子比较 tail 指针是否还是指向自己并更新为 NULL 的操作实现。如果成功地将 tail 指针更新为 NULL,说明没有直接后继,那么跳到第 3 步。
- A 有直接后继 B,先需要等待 B 将链表构建完整,然后检查 B 是不是写者,若是,则将锁结构中的 next_writer 指针指向 B。
- A 判断自己是否是最后一个持有者,若是,且 next_writer 指针不为 NULL,则应该通知 next_writer。具体做法是:A 原子递减 nr_readers,如果 nr_readers 新值大于 0,说明还有读者持有锁,于是 A 直接拍拍屁股走人。如果 nr_readers 新值等于 0,这并不能说明 A 在后面通知 next_writer 时还是最后一个持有者,因为在 A 执行后续操作的间隙中可能依次来了新的读者 C 和 D。假定读者 D 以极快的速度获得并执行释放锁,将 tail 指针置为 NULL。此刻又来了写者 W,因为 W 发现 tail 为 NULL,那么 W 必须将 next_writer 置为自己,否则 A 或 D 无法通知 W。现在轮到 A 继续执行了,A 发现 next_writer 不为 NULL,于是试图通知 W 结束自旋,但是 C 仍然持有锁,导致错误。这个例子说明通知 next_writer 前还需要再检查一下 nr_readers 的值,如果 nr_reader 仍然等于 0,才能说明是最后一个持有锁的读者。如果 C 也迅速释放锁,那么 A 和 C 可能同时试图通知 W。通知一个已经退出自旋的线程是危险的,因为该线程可能重新申请锁,而导致提前退出自旋。因此 A 通知 W 时,需要检测 next_writer 指针是否没有变化并原子地将其置为 NULL,这样 C 会发现已经有人通知过 W 了。
A 是写者,申请锁:
- A 使用原子交换操作将读写自旋锁的 tail 指针指向自己的 qnode 结构以确定在链表中的位置,并返回原来的 tail 值作为自己的直接前驱 pred。
- 如果 pred 等于 NULL,说明锁无人持有或仍有读者持有。A 先将 next_writer 指向自己,然后检查 nr_readers,如果 nr_readers 大于 0,说明尚有读者持有锁,那么自旋等待。如果 nr_readers 等于 0,也有可能某个(些)读者正在释放锁,因此 A 需要检测 next_writer 指针是否仍然指向自己并原子地将其置为 NULL。如果成功置为 NULL,获得锁并返回;否则等待前面的读者通知自己。
- 如果 pred 不等于 NULL,那么 A 标记自己的角色并将链表构建好,然后自旋等待。
A 是写者,释放锁:
- A 先检查是否存在直接后继。这可以用原子比较 tail 指针是否还是指向自己并更新为 NULL 的操作实现。如果成功地将 tail 指针更新为 NULL,说明没有直接后继,那么直接返回。
- A 有直接后继 B,先需要等待 B 将链表构建完整,然后通过 next 指针通知 B 结束自旋。如果 B 是读者,还需要先原子递增 nr_readers。
具体代码如清单 5 所示,有兴趣的读者朋友可以尝试证明一下正确性。
清单 1. 基于单向链表的公平读写自旋锁的实现
#define NONE 0 #define READER 1 #define WRITER 2 typedef struct _qnode{ int type; union { volatile int state; //(1) struct { volatile short waiting; volatile short successor_type; }; }; _qnode *volatile next; } __cacheline_aligned_in_smp qnode; typedef struct { qnode *volatile tail; qnode *volatile next_writer; atomic_t nr_readers; } rwlock_t void init_lock(rwlock_t *lock) { lock->tail = NULL; lock->next_writer = NULL; lock->nr_readers = ATOMIC_INIT(0); } void reader_lock(rwlock_t *lock) { qnode *me = get_myself(); //(2) qnode *pred = me; me->type = READER; me->next = NULL; me->state = 1;// successor_type == NONE && waiting == 1 xchg(&lock->tail, pred); if (pred == NULL) { atomic_inc(&lock->nr_readers); me->waiting = 0; } else { If ((pred->type == WRITER) || (cmpxchg(&pred->state, 1, 0x00010001) == 1)} { //(3) pred->next = me; while (me->waiting) cpu_relax(); } else { atomic_inc(&lock->nr_readers); pred->next = me; me->waiting = 0; } } if (me->successor_type == READER) { while (me->next == NULL) cpu_relax(); atomic_inc(&lock->nr_readers); me->next->waiting = 0; } } void reader_unlock(rwlock_t *lock) { qnode *w; qnode *me = get_myself(); if ((me->next != NULL) || (cmpxchg(&lock->tail, me, NULL) != me)) { while (me->next == NULL) cpu_relax(); if (me->successor_type == WRITER) lock->next_writer = me->next; } if ((atomic_dec_return(&lock->nr_readers) == 1) && ((w = lock->next_writer) != NULL) && (atomic_read(lock->nr_readers) == 0) && (cmpxchg(&lock->next_writer, w, NULL) == w)) w->waiting = 0; } void writer_lock(rwlock_t *lock) { qnode *me = get_myself(); qnode *pred = me; me->type = WRITER; me->next = NULL; me->state = 1; xchg(&lock->tail, pred); if (pred == NULL) { lock->next_writer = me; if ((atomic_read(lock->nr_readers) == 0) && (cmpxchg(&lock->next_writer, me, NULL) == me)) me->waiting = 0; } else { pred->successor_type = WRITER; smp_wmb(); //(4) pred->next = me; } while (me->waiting) cpu_relax(); } void writer_unlock(rwlock_t *lock) { qnode *me = get_myself(); if ((me->next != NULL) || (cmpxchg(&lock->tail, me, NULL) != me)) { while (me->next == NULL) cpu_relax(); if (me->next->type == READER) atomic_inc(&lock->nr_readers); me->next->waiting = 0; } } |
- 使用 union 结构是为了方便后面将 successor_type 和 waiting 合在一起做原子操作。
- 如果事先知道线程的数目,例如代码用于中断上下文,qnode 可以包装在 lock 数据结构中,每个线程一个;否则,可以使用线程本地存储区(Thread Local Storage,使用 gcc 关键字 __thread)分配 qnode。我们不关心 qnode 的分配细节,假定有个 get_myself() 函数可以获得当前线程的 qnode。
- cmpxchg 原子操作将 [successor_type,waiting] 原子地从 [NONE,TRUE] 改变为 [READER,TRUE]。
- 此处的“Write Memory Barrier”目的是确保对 successor_type 的赋值先完成。因为这两个赋值没有相关性,如果处理器乱序执行写指令,且对 next 的赋值先完成,那么链表就已构建完毕,前驱可能随时释放锁导致 pred 指针失效。
基于单向链表的读写自旋锁并不完美,因为线程还是得操作额外的共享变量 nr_readers 和 next_writer,这是由于读者释放锁的时候无法继续保持单向链表的结构。一种改进想法是使用双向链表 [5],因为双向链表的插入和删除操作能够做得相对高效。于是读者释放锁的时候可以将自己的节点结构从双向链表中删除,从而继续保持链表的结构。读者通过判断前驱指针是否为 NULL 就知道自己是不是最后一个持有者,而且也再不需要 next_writer 指针。但是该算法中对双向链表的操作使用了节点级别的细粒度普通自旋锁,在连续读者较多且几乎同时释放读写自旋锁的情况下,同步开销依然巨大。
第二种想法是为每个线程分配一个局部的普通自旋锁,读者只需获得自己的自旋锁即可,而写者必须将包括自己在内的所有人的自旋锁都获得才行 [6]。这个算法显然是个读者优先的实现,在写者较少的情况下可扩展性相当理想。不足之处有两点:一是写者得知道其它线程的自旋锁在哪儿,因此比较适用于固定线程的场景;其次是读者数目越多,对写者也就越不公平。
我们看到:一般而言,在一段可观测的时间内,读者数量远远大于写者,很多时候甚至没有写者。因此在基于单向链表的实现中,只有共享变量 nr_readers 才是一个明显的瓶颈。进一步分析可知,我们其实并不需要知道 nr_readers 的具体值,只是想了解 nr_readers 是否大于 0 还是等于 0,于是 Sun 的研究人员提出使用一种称为可扩展非零指示器(Scalable Non-Zero Indicator)的数据结构,大大降低了线程间的同步开销 [7]。
本系列文章详细论述读写自旋锁的原理和实现,本文是其中的第三部分,针对大规模多核系统讨论如何设计和实现可扩展的读写自旋锁。
发表评论
-
读写自旋锁详解,第 1 部分
2011-08-29 12:22 1118引用:http://www.ibm.com/d ... -
des和3Des加密算法实现
2011-04-24 15:57 4341DES简介: DES算法为密码体制中的 ... -
jstack 和jmap
2011-04-23 20:47 2887sun提供的jvm检测工具jstack和jms ... -
理解Heap Profling名词-Shallow和Retained Sizes
2011-04-23 00:40 944所有包含Heap Profling功能的工具(MAT, Y ... -
Boosting算法简介
2011-04-22 23:52 1564一、Boosting算法的发展历史 Boosting ... -
jtrace
2011-04-22 00:04 895... -
jsoup使用
2011-04-20 22:28 5026jsoup 是一 ... -
java序列化
2011-04-15 23:42 890引言 将 Java 对象序列化为二进制文件的 Java ... -
用javap分析java编译器对string常量表达式的处理和优化
2011-01-06 16:25 1157最近看了下javaeye上一篇关于string优化的文章 ... -
转【JAVA】虚拟机指令集
2011-01-05 11:19 16360x00 nop 什么都不做0x01 acons ... -
检查字符串是否是整数
2010-12-31 10:57 1585检查字符串是否是整数 今天在论坛中看见一个贴,讨论用异常 ... -
[转] 软件体系架构模式在J2EE中的应用(管道与过滤器模式)
2010-12-13 11:11 1051本文介绍了软件体系架构产生的背景和架构模式的基本理论.重点 ...
相关推荐
### 信号量、互斥体和自旋锁的区别详解 #### 一、基本概念与应用场景 **信号量**、**互斥体**和**自旋锁**是操作系统中三种常用的同步机制,主要用于解决多线程或多进程环境中资源的并发访问问题。这三种机制虽然...
### 原子操作、信号量、读写信号量和自旋锁的API详解 #### 一、引言 在现代操作系统中,特别是在多处理器环境下,确保数据的一致性和完整性至关重要。为此,Linux内核提供了多种同步机制来保护共享资源免受并发...
- **自旋锁**:如果线程在尝试获取锁时发现锁已被占用,但很快就会释放,线程会选择自旋等待而不是进入阻塞状态,减少上下文切换。 5. **显式锁(java.util.concurrent.locks包)**: - **ReentrantLock**:可重...
同时,书中也会涉及信号量、自旋锁、读写锁等同步原语的使用方法。 对于块设备和字符设备驱动,ldd3会有专门的章节进行讲解。字符设备通常用于处理低级的输入/输出操作,而块设备则适用于处理磁盘和其他存储设备。...
当一个线程试图获取一个已经被其他线程持有的自旋锁时,它会持续循环检查锁是否可用,而不是进入睡眠状态。这种机制适用于快速锁定和解锁的情况,但在长时间持有锁的情况下会导致性能问题。 - **信号量**是一种更...
3. **自旋锁(Spinlock)**:自旋锁适用于短时间的锁定,当锁被占用时,等待的线程不会被挂起,而是持续检查锁是否可用,一旦可用立即获取,减少了上下文切换的开销。 4. **信号量(Semaphore)**:信号量是一种更...
当一个线程尝试获取自旋锁但发现锁已被占用时,它会进入循环(自旋),持续检查锁是否可用,而不是被调度到内核队列中。`spin_lock()` 和 `spin_unlock()` 函数用于操作自旋锁。 3. 原子操作(Atomic Operations)...
- **读写自旋锁**:解释读写自旋锁的使用场景。 - **顺序锁**:介绍顺序锁的概念及其应用场景。 - **读-复制-更新**:探讨RCU(Read-Copy-Update)机制在并发控制中的作用。 - **信号量**:解释信号量的工作原理...
接着,深入探讨了线程同步机制,如互斥锁、条件变量、读写锁和自旋锁的工作原理及具体应用示例。此外,还介绍了线程优先级、调度策略、线程局部存储和信号量等相关概念和技术。最后,通过生产者消费者问题和读者写者...
第81讲:Netty引用计数的实现机制与自旋锁的使用技巧 第82讲:Netty引用计数原子更新揭秘与AtomicIntegerFieldUpdater深度剖析 第83讲:AtomicIntegerFieldUpdater实例演练与volatile关键字分析 第84讲:Netty...
- **并发控制机制**:包括中断屏蔽、原子操作、自旋锁、读写自旋锁、顺序锁、读-复制-更新、信号量、互斥体和完成量,都是为了解决多线程或中断环境下资源访问的安全性问题。 8. **阻塞与非阻塞I/O** - **阻塞与...
`while(isLocked)`循环的使用被称为“自旋锁”,线程会不断尝试获取锁,直到成功为止,提高了响应速度但可能会消耗更多CPU资源。 锁的可重入性是Java中非常重要的特性,这意味着一个线程可以进入已获得其锁的对象的...
8. **锁和同步机制**:为了保证多线程环境下的正确性,内核使用了各种同步原语,如自旋锁、信号量、读写锁等,防止数据竞争。 9. **模块化设计**:Linux内核允许模块化加载,使得非核心功能可以按需加载,提高了...
除此之外,Linux内核还提供了原子操作、读写信号量、大内核锁、读写锁、大读者锁、RCU(Read-Copy Update)和顺序锁等同步机制。 2. Linux的用户模式与内核模式: Linux操作系统采用双模式运行,即用户模式和内核...
读核感悟 同步问题 内核态自旋锁的实现 56 1自旋锁的总述 56 2非抢占式的自旋锁 56 3 锁的释放 57 4 与用户态的自旋锁的比较 57 5 总结 58 读核感悟 内存管理 free命令详解 58 读核感悟 文件读写 2 6 9内核中的AIO ...
自旋锁是一种简单的同步原语,当锁被占用时,尝试获取锁的线程不会进入睡眠状态,而是持续检查锁是否可用。一旦锁释放,线程立即获得锁并继续执行。自旋锁适用于锁的持有时间很短的情况,以减少上下文切换的开销。 ...
`synchronized`在JVM层面是基于监视器锁(Monitor)实现的,依赖于操作系统的Mutex lock(互斥锁),早期版本性能较低,但1.5以后通过一系列优化,如锁粗化、锁消除、轻量级锁、偏向锁和自旋锁等,性能得到了显著提升...
【标题】:PostgreSQL系统锁详解 在数据库管理系统中,锁是用于控制多个并发操作的重要机制,确保数据的一致性和完整性。PostgreSQL作为一款强大的开源关系型数据库,其内部实现了一套完善的锁定机制,名为“syslck...
锁机制详解 锁机制是指在并发控制中,为了避免数据冲突和不一致,使用的机制来控制多个线程或用户对共享资源的访问。锁机制可以分为悲观锁和乐观锁两种。 一、悲观锁 悲观锁是一种对数据的修改抱有悲观态度的并发...