CAS lock-free
转:http://www.cnblogs.com/lucifer1982/archive/2009/04/08/1431992.html
http://en.wikipedia.org/wiki/Compare-and-swap
In computer science, compare-and-swap (CAS) is an atomic instruction used in multithreading to achieve synchronization. It compares the contents of a memory location to a given value and, only if they are the same, modifies the contents of that memory location to a given new value. This is done as a single atomic operation. The atomicity guarantees that the new value is calculated based on up-to-date information; if the value had been updated by another thread in the meantime, the write would fail. The result of the operation must indicate whether it performed the substitution; this can be done either with a simple Boolean response (this variant is often called compare-and-set), or by returning the value read from the memory location (not the value written to it).
The following C function shows the basic behavior of a compare-and-swap variant that returns the old value of the specified memory location; however, this version does not provide the crucial guarantees of atomicity that a real compare-and-swap operation would:
int compare_and_swap (int* reg, int oldval, int newval)
{
int old_reg_val = *reg;
if (old_reg_val == oldval)
*reg = newval;
return old_reg_val;
}
lock-free 编程实在让人又爱又恨。博主以前曾经写过几篇关于 lock-free 编程的文章。比如关于无锁编程、并发数据结构:迷人的原子。如果想更加深入的了解和实践 lock-free 编程,可以参考CLR 2.0 Memory Model、并发数据结构:Stack。这篇文章并不打算继续阐述如何使用 lock-free 技术,而是谈一下它的负面影响。从而让大家对 lock-free 有个更加全面的认识。
说到 lock-free 编程,现实中经常使用 CAS 原语。CAS 是英文 Compare and Swap 的简写。在 Windows 和 .NET 平台,由于历史原因,它被写做 Interlocked API。原子操作在 x86 架构 CPU 对应的汇编指令有 XCHG、CMPXCHG、INC 等,当然还得加上 LOCK 作为前缀(更多信息请看 并发数据结构:迷人的原子)。
CAS 原语在轻度和中度争用情况下确实可以大幅度提高程序性能。但凡事有利必有弊,CAS 原语极度扼杀了程序的可伸缩性(其他缺点请看关于无锁编程)。各位看官可能觉得这种观点有点偏激,但事实如此。请容博主细细道来:
- CAS 的原子性完全取决于硬件实现。大多数 Intel 和 AMD 的 CPU 采用了一种叫做 MOSEI 缓存一致性协议来管理缓存。这种架构下,处理器缓存内 CAS 操作相对成本低廉。但一旦资源争用,就会引起缓存失效和总线占用。缓存越失效,总线越被占用,完成 CAS 操作也越被延迟。缓存争用是程序可伸缩性杀手。当然对于非 CAS 内存操作来说也是如此,但 CAS 情况更加槽糕。
- CAS 操作要比普通内存操作花费更多 CPU 周期。这归功于缓存分级的额外负担、刷新写缓冲区与穿越内存栅栏限制和需求以及编译器对 CAS 操作优化的能力。
- CAS 经常被用在优化并行操作上。这意味着 CAS 操作失败将导致重新尝试某些指令(典型的回滚操作)。即便没有任何争用,它也会做一些无用功。不论成功或失败都会增加争用的风险。
大多数 CAS 操作发生在锁进入和退出时。尽管锁可由单一 CAS 操作构建,但 .NET CLR Monitor 类却使用了两个(一个在 Enter 方法,另一个在 Exit 方法)。lock-free 算法也经常使用 CAS 原语来代替使用锁机制。但是由于内存重组,这样的算法也常常需要显式的栅栏,即便使用了 CAS 指令。锁机制非常邪恶,但大多数合格的开发人员都知道让锁持有尽量少的时间。因此,虽然锁机制让人非常讨厌,且影响性能。但相对于大量,频繁的 CAS 操作而言,它却并不影响程序的可伸缩性。
举个很简单的例子,增加计数 100,000,000 次。要做到这样,有几种方式。如果仅运行在单核单处理器上,我们可以使用普通的内存操作:
static volatile int counter = 0;
static void BaselineCounter()
{
for (int i = 0; i < Count; i++)
{
counter++;
}
}
很明显,上述代码示例不是线程安全的,但给计数器提供了一个很好的时间基准。下面我们使用 LOCK INC 来作为线程安全的第一种方式:
static volatile int counter = 0;
static void LockIncCounter()
{
for (int i = 0; i < Count; i++)
{
Interlocked.Increment(ref counter);
}
}
现在代码示例线程安全了。我们还可以采取另外一种方式来保证线程安全。如果需要执行一些验证(比如内存溢出保护),我们通常会使用这种方式。就是使用 CMPXCHG(即 CAS):
static volatile int counter = 0;
static void CASCounter()
{
for (int i = 0; i < Count; i++)
{
int oldValue;
do
{
oldValue = counter;
}
while (Interlocked.CompareExchange(ref counter, oldValue + 1, oldValue) != oldValue);
}
}
现在问一个有意思的问题:当缓存争用时,哪一个方法更慢?结果可能会让你大吃一惊哦。
在 Intel 4 核处理器下测试结果如下:
图中,当 CPU 使用 2 个核时,BaselineCounter 方法是单核单路情况的 2.11 倍。其他情况类似。通过结果比对,我们可以得知:更多的并发性导致结果更加槽糕。这很大部分原因由内存争用所致。
当 CAS 操作失败,通过旋转等待可以改善 CASCounter 方法的在多核处理器上的性能(具体技巧可以参考夏天是个好季节兄的自己动手实现一个轻量级的信号量(一)、(二))。这可以大大减少活锁和关联内联阻碍锁耗费的时间。
当然,这个示例非常极端。它频繁反复修改同一个内存地址。通过期间插入特定的函数调用,延迟访问共享内存可以极大缓解压力。
比如插入 2 个函数调用,我们得到了如下数据:
插入 64 个函数调用之后,数据又变成了如下所示:
这个时候,我们看到多核所花费的时间少于单核了。这就是我们使用并行所带来的加速。看到这里,我们可能会想,既然从 2 到 64 个函数调用使得结果越来越好,那么超过 64 个函数调用岂不是会变得更好?实际上,在插入 128 个函数调用之后,加速已经达到极限。结果如下所示:
如何计算加速比,请参考并行思维 [II]。
天下没有免费的午餐,CAS 也不例外。我们应当慎之又慎的将 lock-free CAS 代码放到我们的代码中,且必须清楚的知道线程执行它们的频繁程度。我们可以用下面这句话来作为总结:共享是魔鬼。它从根本上限制应用程序可伸缩性,最好尽量避免。共享内存需要并发控制,而并发控制需要 CAS。CAS 又非常昂贵,因此共享内存也非常昂贵。有很多人提出 lock-free 技术,事务内存,读写锁等可以改善程序可伸缩性。但很遗憾,这种情况很少出现。CAS 往往比正确实现锁机制的解决方案更加糟糕。很大原因要归结于共享内存、乐观失败尝试、缓存失效等。
Update 于 2009 年 4 月 8 日 21 : 10
overred 兄在 review 这篇文章的时候,提了一个很好的问题:在使用 Interlocked API 的时候,共享变量不用 volatile 修饰。
为了更方便说明这个问题,俺写个简单点的代码示例,如下所示:
using System; namespace Lucifer.CSharp.Sample
{
class Program
{
static volatile int x; static void Main(string[] args)
{
Foo(ref x);
} static void Foo(ref int y)
{
while (y == 0) ;
}
}
}
当我们在 Visual Studio 中编译这段代码时,IDE 会给出编译警告,如下所示:
通常来说,我们对于这样的编译警告应该给予足够重视。比如在上面的例子中,JIT 编译器会认为 y 一直未变,从而引起死循环。在 IA64 平台上,这会被认为普通内存访问代替了特殊的 load-acquire 访问,这就可能导致 CPU 指令重组方面的一些 Bug。但是有一种情况例外,就是使用 Interlocked API 和 Thread.VolatileXXX 方法以及锁。因为这些 API 内部都会显式要求内存栅栏和硬件原子指令,而不管外部共享变量是否采用 volatile 修饰。因此,文中采用的测试方法还是很安全嘀。
如果你觉得这个编译警告很烦人,可以使用 #pragma 指令禁掉这种警告,如下所示:
static volatile int x; static void Foo()
{
#pragma warning disable 0420
Interlocked.Exchange(ref x, 1);
#pragma warning restore 0420
}
当然,也可以完全不用 volatile 修饰符。CLR 内存模型保证了这一点。
如何正确使用 volatile ,请参考并发数据结构:谈谈volatile变量。
CAS lock-free的更多相关文章
- redis实现分布式可重入锁
利用redis可以实现分布式锁,demo如下: /** * 保存每个线程独有的token */ private static ThreadLocal<String> tokenMap = ...
- Java并发计数器探秘
前言 一提到线程安全的并发计数器,AtomicLong 必然是第一个被联想到的工具.Atomic* 一系列的原子类以及它们背后的 CAS 无锁算法,常常是高性能,高并发的代名词.本文将会阐释,在并发场 ...
- Java对象内存布局
本文转载自Java对象内存布局 导语 首先直接抛出问题 Unsafe.getInt(obj, fieldOffset)中的fieldOffset是什么, 类似还有compareAndSwapX(obj ...
- cas aqs lock之间的关系
CAS 对应cpu的硬件指令, 是最原始的原子操作 cas主要是在AtomicInteger AtomicXXX类中使用, 用于实现线程安全的自增操作 ++. 对应一个unsafe对象, 根据os平台 ...
- java里的锁总结(synchronized隐式锁、Lock显式锁、volatile、CAS)
一.介绍 首先, java 的锁分为两类: 第一类是 synchronized 同步关键字,这个关键字属于隐式的锁,是 jvm 层面实现,使用的时候看不见: 第二类是在 jdk5 后增加的 Lock ...
- 装逼名词-ABA CAS SpinLock
今天看wiki,看到一个提到什么什么会陷入 race condition & ABA problem.丫的我没听过ABA呀,那么我去搜了一下,如下: http://www.bubuko.com ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantLock之二lock方法分析
前一篇博客简单介绍了ReentrantLock的定义和与synchronized的区别,下面跟随LZ的笔记来扒扒ReentrantLock的lock方法.我们知道ReentrantLock有公平锁.非 ...
- Java的多线程机制系列:(二)缓存一致性和CAS
一.总线锁定和缓存一致性 这是两个操作系统层面的概念.随着多核时代的到来,并发操作已经成了很正常的现象,操作系统必须要有一些机制和原语,以保证某些基本操作的原子性.首先处理器需要保证读一个字节或写一个 ...
- JUC.Lock(锁机制)学习笔记[附详细源码解析]
锁机制学习笔记 目录: CAS的意义 锁的一些基本原理 ReentrantLock的相关代码结构 两个重要的状态 I.AQS的state(int类型,32位) II.Node的waitStatus 获 ...
- JAVA CAS原理深度分析-转载
参考文档: http://www.blogjava.net/xylz/archive/2010/07/04/325206.html http://blog.hesey.net/2011/09/reso ...
随机推荐
- saltstack 开发
最近跟总部一个项目用saltstack封装api来用,可以很轻松的实现restful api发布,提供别人直接调用 salt 本身有2个内置变量(__salt__, __opts__),通过salt ...
- 深入理解javascript函数系列第三篇
前面的话 函数是javascript中特殊的对象,可以拥有属性和方法,就像普通的对象拥有属性和方法一样.甚至可以用Function()构造函数来创建新的函数对象.本文是深入理解javascript函数 ...
- BZOJ1004 HNOI Cards
第一次学习置换群这个东西. 这题需要利用Burnside定理. 即我们求出循环节为一(转完不变)的个数的平均数也就是等价类的个数. 定义:设G={a1,a2,…ag}是目标集[1,n]上的置换群.每个 ...
- [BZOJ4320][ShangHai2006]Homework(根号分治+并查集)
对于<=sqrt(300000)的询问,对每个模数直接记录结果,每次加入新数时暴力更新每个模数的结果. 对于>sqrt(300000)的询问,枚举倍数,每次查询大于等于这个倍数的最小数是多 ...
- 【动态规划/多重背包问题】POJ1014-Dividing
多重背包问题的优化版来做,详见之前的动态规划读书笔记. dp[i][j]表示前i中数加得到j时第i种数最多剩余几个(不能加和得到i的情况下为-1)递推式为: dp[i][j]=mi(dp[i-1][j ...
- 【洛谷】P1176: 路径计数2【递推】
P1176 路径计数2 题目描述 一个N×N的网格,你一开始在(1,1),即左上角.每次只能移动到下方相邻的格子或者右方相邻的格子,问到达(N,N),即右下角有多少种方法. 但是这个问题太简单了,所以 ...
- bzoj4753 最佳团体
题目描述 JSOI 信息学代表队一共有 NN 名候选人,这些候选人从 11 到 NN 编号.方便起见,JYY 的编号是 00 号.每个候选人都由一位编号比他小的候选人R_iRi 推荐.如果 R_i ...
- 2349 Arctic Network(中文版)
试题描述: 国防部希望通过无线网络连接几个北方前哨基地. 在建立网络时将使用两种不同的通信技术:每个前哨基站都将拥有无线电收发器,另外还有一些前哨卫星通道. 任何带卫星频道的两个前哨都可以通过卫星进行 ...
- 参加SAP VT项目有感
凡事预则立,不预则废. 没有接到录取电话还是有些悲伤的,虽然知道最终被录取的可能性不大,但是之前还是抱着一丝期望的,毕竟是自己的处女面,就这么以失败的结果结束了. 从最开始的投递简历,到后来的电话面试 ...
- Java中应该返回零长度数组或空集合,而不是返回null(转)
说明:为了避免在数组和集合的获取上增加null的判断,同时也能减少不必要的空指针异常,通常会在业务返回零数组或空集合. 方法: 1.数组: 定义全局静态常量来减少内存开销:private static ...