Ticket Lock的Relaxed Atomics优化

原文地址 作者:Pedro Ramalhete,译者:周可人,校对:梁海舰

Tick lock是mutual lock的一种简单实现:

http://web.mit.edu/6.173/www/currentsemester/readings/R06-scalable-synchronization-1991.pdf

它是由John Mellor-Crummey和Michael Scott在1991年提出的(pdf中2.2节),感谢C++11和C11中新的内存模型,我们可以对单个变量进行具有屏障或者不具有屏障的原子操作。当屏障没有使用,只有原子性保证时,我们称之为“relaxed atomic”:

http://en.cppreference.com/w/cpp/atomic/memory_order

注意在C11/C++11内存模型中,一个原子变量并不具有内在的“顺序一致性”或者“relaxed”性质,然而我们可以在每次访问的时选择它的行为。

原子修饰符只能保证原子性,即这个变量会被单步读或写。其他语言,如Java和Scala则不同,它们可以保证几乎所有的原生类型提供原子性保证,从而表现为“relaxed atomic”。并且,所有被声明为顺序一致性的变量可以在整个程序中保持性质(除非在Java中使用sun.misc.unsafe)。尽管这个细微的差异可能看起来并不重要,但是当我们的目标是从同步或是并发算法中挖掘最大性能时,就需要关注这个差异了。

假设你想要在不同语言中声明一个原子整型,下面是你可能会做的:

01 C11 (can be relaxed or sequentially consistent)
02  
03 _Atomic int x;
04  
05 C++11 or C++14 (can be relaxed or sequentially consistent)
06  
07 atomic<int> x;
08  
09 Java (sequentially consistent)
10  
11 volatile int x;

当具备上述信息时,我们可以写出一个简单的Ticket Lock C11版本的实现,如下所示:

01 typedef struct
02  
03 {
04  
05     _Atomic long ingress;
06  
07     char padding[64];      // To avoid false sharing between ingress and egress
08  
09     _Atomic long egress;
10  
11 } ticket_mutex_t;
12  
13   
14  
15 void ticket_mutex_lock(ticket_mutex_t * self)
16  
17 {
18  
19     long lingress = atomic_fetch_add(&self->ingress, 1);
20  
21     while (lingress != atomic_load(&self->egress)) {
22  
23         sched_yield();
24  
25     }
26  
27     // This thread has acquired the lock on the mutex
28  
29 }
30  
31   
32  
33 void ticket_mutex_unlock(ticket_mutex_t * self)
34  
35 {
36  
37     atomic_fetch_add(&self->egress, 1);
38  
39 }

 

这里有3处不明显的优化,我们可以在上述代码的基础上利用releaxed atomics实现。让我们一个一个看。

  1. 在lock方法中,当egress变量第一次被读取的时候,我们可以用relaxed load优化。

代码如下:

01 void ticket_mutex_lock(ticket_mutex_t * self)
02  
03 {
04  
05 long lingress = atomic_fetch_add(&self->ingress, 1);
06  
07 // If the ingress and egress match, then the lock as been acquired and
08  
09 // we don't even need to do an acquire-barrier.
10  
11 if (lingress == atomic_load_explicit(&self->egress, memory_order_relaxed)) return ;
12  
13 while (lingress != atomic_load(&self->egress)) {
14  
15 sched_yield();  // Replace this with thrd_yield() if you use <threads.h>
16  
17 }
18  
19 // This thread has acquired the lock on the mutex
20  
21 }

我们这样做的原因是atmoic_fetch_add(ingress)将会获取一个barrier,这代表着atomic_load_explicit(egress, memory_order_relaxed)将永远不能在atomic_fetch_add()之前被执行,但是它可以获得一个最新的egress的值,至少在atomic_fetch_add(ingress)执行之后。这意味着egress变量的值将永远不会比atomic_fetch_add(ingress)所返回的值更大。

在x86架构中,这个优化并不会带来效率提升,因为获得屏障是没有代价的,这代表节省一个屏障并不能带来任何收益。但是在其他架构中——如ARMv8,这个优化将带来少量的收益。当我可以在ARMv8上开发,并且配有支持C11/C++11的gcc时,我会详细说明。

 

  1. 在unlock()方法中,我们可以使用atomic_load()和atomic_store()来替代atomic_fetch_add()。

unlock()方法中并没有在一个原子操作中同时读/写的需求,因为同一时间只有一个线程可以调用这个方法。这说明我们可以这样实现egress变量的增加操作:使用atomic_load()读取egress变量的当前值,然后使用atomic_store()操作将当前值加1写入egress变量。

 

  1. 在前面提到的atomic_load()操作可以是relaxed。

步骤2可以通过relaxed atomic_load()进一步优化。在lock()方法中,atomic_fetch_add()有一个获取屏障的操作,或者在糟糕的情况下,这个屏障在while循环中的atomic_load(),用来保证当前线程获得最新的egress值。我们可以保证在这个屏障和unlock()之间没有其他线程调整egress变量,所以这样做是安全的。

最后的代码如下:

1 void ticket_mutex_unlock( ticket_mutex_t * self)
2  
3 {
4  
5 long legress = atomic_load_explicit(&self-> egress, memory_order_relaxed);
6  
7 atomic_store(&self-> egress, legress+1);
8  
9 }

上述是一些关于如何使用relaxed atomics来优化代码的例子。

现在,我们可以实际地说这些特殊优化所带来的提升是非常小的,并且一些优化是令人费解的,更是难以证明其正确性的。有利的一点是这个代码仍然是跨平台的。你可以在x86,ppc,mips,arm上运行它,因为内存模型的一致性,你有信心保证代码仍然是安全的。

这是我喜爱C11/C++1x内存模型的原因。

可以在github上找到C11 Ticket Lock源代码:

https://github.com/pramalhe/ConcurrencyFreaks/blob/master/C11/locks/ticket_mutex.h

https://github.com/pramalhe/ConcurrencyFreaks/blob/master/C11/locks/ticket_mutex.c

时间: 2024-09-20 04:54:06

Ticket Lock的Relaxed Atomics优化的相关文章

MYSQL METADATA LOCK(MDL LOCK)学习(1) 理论知识和加锁类型测试

原创,水平有限如有错误请指出共同探讨本文中某些结论性的东西我用黄色标记出来请大家多多留意一下另外我家二娃刚刚出生,大家祝福我一下吧.谢谢!^_^ 本文网址 http://blog.itpub.net/7728585/viewspace-2143093/ 源码版本:5.7.14注意MDL和DML术语的不同. 一.前言    MYSQL中MDL锁一直是一个比较让人比较头疼的问题,我们谈起锁一般更加倾向于INNODB下层的gap lock.next key lock.row lock等, 因为它很好理

[转]自旋锁、排队自旋锁、MCS锁、CLH锁

转自:http://coderbee.net/index.php/concurrent/20131115/577 自旋锁(Spin lock) 自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态. 自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短. 简单的实现 import java.util.concurrent.atomic.AtomicReference; public class SpinLoc

《Oracle高性能自动化运维》一一2.3 Library Cache

2.3 Library Cache2.3.1 Library Cache与SQL游标 Library Cache主要用于存放SQL游标,而SQL游标最大化共享是Library Cache优化的重要途径,可以使SQL运行开销最低.性能最优. 1. SQL语句与父游标及子游标 在PL/SQL中,游标(Cursor)是数据集遍历的内存集合.而从广义上讲,游标是SQL语句在Library Cache中的内存载体.SQL语句与游标关系如下: 1)一条SQL语句包含一个父游标(Parent Cursor)和

聊一聊双十一背后的技术 - 不一样的秒杀技术, 裸秒

聊一聊双十一背后的技术 - 不一样的秒杀技术, 裸秒 作者 digoal 日期 2016-11-17 标签 PostgreSQL , 秒杀 , 裸秒 , ad lock 双十一背后的技术系列文章 <聊一聊双十一背后的技术 - 物流, 动态路径规划> <聊一聊双十一背后的技术 - 分词和搜索> <聊一聊双十一背后的技术 - 强奸式秒杀技术实现> <聊一聊双十一背后的技术 - 毫秒分词算啥, 试试正则和相似度> 云栖聚能聊 - 聊一聊双十一背后的数据库技术 背景

聊聊全站HTTPS带来的技术挑战

日前写的文章里了讨论了数据传输的安全性的问题,最后一部分提到了通过HTTPS解决数据传输安全性的方案.那么一个新问题又来了,实施全站HTTPS的过程中,我们可能会遇到哪些技术问题?所以我今天和大家一起来算一下这个账,将技术成本理清楚. 准备工作 购买证书 ,网站使用HTTPS需要申请安全证书,目前来说还是比较繁琐的,而且对小公司来说是有一些成本在.另外,一定要选正规的机构,否则你的网站以后使用主流浏览器,如chrome访问,会被提示大大的警告,告诉用户该证书有问题. 页面里所有资源都要改成走ht

MySQL内核月报 2014.11-MySQL· 5.7优化·Metadata Lock子系统的优化

背景 引入MDL锁的目的,最初是为了解决著名的bug#989,在MySQL 5.1及之前的版本,事务执行过程中并不维护涉及到的所有表的Metatdata 锁,极易出现复制中断,例如如下执行序列: Session 1: BEGIN; Session 1: INSERT INTO t1 VALUES (1); Session 2: Drop table t1; --------SQL写入BINLOG Session 1: COMMIT; -----事务写入BINLOG 在备库重放 binlog时,会

移动App性能测评与优化2.2.2 优化方法二:wake lock

2.2.2 优化方法二:wake lock 前台运行时,不要去注册wake lock.此时注册没有任何意义,却会被计算到应用电量消耗中. 后台运行时,在保证业务需要的前提下,应尽量减少注册wake lock. 降低对系统的唤醒频率.使用partial wake lock代替full wake lock,因为屏幕的亮起,也会消耗手机电量. 在注册后,也要注意及时释放,否则锁的持有时间会被一直计算到电量消耗中.

MySQL5.7 : Metadata Lock关键类及函数

在MySQL5.7.4中,对Server层的Metadata Lock做了颠覆性的修改,在正常负载下几乎完全消除了MDL子系统部分的锁开销.   基于最新的MySQL5.7.5, 我们来看看MDL锁的具体实现思路.注意在5.7.5里,MDL子系统的代码和之前版本已经发生了非常大的差异,具体表现在: 1.    针对DDL 和DML做了区分,引入了fast path的概念,对于走fast path路径的MDL锁,无需任何读写锁操作,使用类似LOCK WORD的方式来标记赋予权限 2.    对MD

Linux转发瓶颈分析、评估、优化与解决方案

线速概念 很多人对这个线速概念存在误解.认为所谓线速能力就是路由器/交换机就像一根网线一样.而这,是不可能的.应该考虑到的一个概念就是延迟.数据包进入路由器或者交换机,存在一个核心延迟操作,这就是选路,对于路由器而言,就是路由查找,对于交换机而言,就是查询MAC/端口映射表,这个延迟是无法避开的,这个操作需要大量的计算机资源,所以不管是路由器还是交换机,数据包在内部是不可能像在线缆上那样近光速传输的.类比一下你经过十字街头的时候,是不是要左顾右盼呢?        那么,设备的线速能力怎么衡量呢