并发编程:Disruptor为什么这么快,锁的缺点

作为开发者,听到线程这个词,首先想到的是并发,但是并发并不容易。

假如两个线程尝试修改同一个变量value:

情况一:线程1先到达
情况二:线程2先到达
情况三:线程1和线程2交互

1、 Entry value= “fluff”

2、 Thread 1 setvalue(“blah”)

3、Thread 2 :
myvalue:getvalue()
。。。
setvalue(“myvalue + “y”)

情况三显然是唯一一个错误的,除非你认为wiki编辑的幼稚做法是正确的。其他两种情况主要看你的意图和想要达到效果。在一定的前提下,情况一和情况二都是正确的。

解决该问题的办法:

办法一:悲观锁

1、 Entry value= “fluff”

2、 Thread 1 setvalue(“blah”)

3、Thread 2 :
myvalue:getvalue()
。。。
setvalue(“myvalue + “y”)
悲观锁和乐观锁这两个词通常在我们谈论数据库时经常会用到,但原理可以应用到在获得一个对象的锁的情况。只要线程2获得Entry的互斥锁,它就会阻击其他线程去改变它,然后它就可以随意做它想要做的事情,设置值,然后想干嘛干嘛。
你可以想象这里非常耗性能,因为其他线程在系统各处徘徊着准备要获得锁然后又阻塞,线程越多,系统的响应性能会越慢。

办法二:乐观锁

1、 Entry value= “fluff”

2、 Thread 1 setvalue(“blah”)

3、Thread 2 :
myvalue:getvalue()
。。。
if (value not changed())
setvalue(myvalue + “y”)
在这种情况,当线程2需要去写Entry时才会去锁定它,它需要坚持Entry自从上次读过后是否已经被改过了。如果线程1在线程2读完后到底并把值改为”blah”,线程2读到了这个新值,线程2不会把“fluffy”写到Entry里并把线程1所写的数据覆盖,线程2会重试(重新读新的值,与旧值比较,如果相等则在变量的值后面附上“y”), 这里在线程2不会关系新值是什么的情况。或者线程2会抛出一个异常,或者会返回一个某些字段已更新的标志,这是在期望把“fluff”改为“fluffy”的情况。举一个第二种情况的例子,如果你和另外一个用户同时更新一个wiki的页面,你会告诉另外一个用户的线程Thread2,他们需要重新加载从Thread1来新的变化,然后再提交他们的内容。

潜在的问题:死锁

锁定会带来各种各样的问题,比如死锁,想象有2个线程需要访问两个资源,如果你滥用锁技术,两个锁都在获得锁的情况下尝试去获得另外一个锁,那就是需要自我检讨的时候了。

很明确的问题:锁技术是很慢的。。。。。

关于锁就是它们需要操作系统去做裁定。

Disruptor论文中讲述了我们所做的一个实现。这个测试程序调用了一个函数,该函数会对一个64为的计数器循环自增5亿次。当单线程无锁时,程序耗时300ms。如果增加一个锁(仍是单线程、没有竞争、仅仅增加锁),程序需要耗时10000ms,慢了两个数量级。更令人吃惊的是,如果增加一个线程(简单从逻辑上想,应该比单线程加锁快一倍),耗时224000ms。使用两个线程对计数器自增5亿次比使用无锁单线程慢1000倍。并发很难而锁的性能糟糕。我仅仅是揭示了问题的表面,而且,这个例子很简单。但重点是,如果代码在多线程环境中执行,作为开发者将会遇到更多的困难:

a、代码没有按设想的顺序执行。上面的场景3表明,如果没有注意到多线程访问和写入相同的数据,事情很可能会很糟糕。
b、减慢系统的速度。场景3中,使用锁保护代码可能导致诸如思索或者效率问题。

这就是为什么许多公司在面试时会多少问些并发问题。不幸的是,即使未能理解问题的本质或没有问题解决方案,也很容易学会如何回答这个问题。

Disruptor如何解决这些问题。

首先,Disruptor根本就不用锁。
取而代之的是,在需要确保操作是线程安全的(特别是,在多生产者的环境下,更新下一个可用的序列号)地方,我们使用CAS(Compare And Swap/Set)操作。这是一个cpu级别的指令,在我的意识中,它的工作方式有点像乐观锁–CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其他操作就先改变了这个值。

注意,这可以是CPU的两个不同的核心,但不会是两个独立的CPU.

CAS操作比锁消耗资源少的多,因为它们不牵涉操作系统,它们直接在CPU上操作。但它们并非没有代价–在上面的实验中,单线程无锁耗时300ms,单线程有锁耗时10000ms,单线程使用CAS耗时5700ms。所以它比使用锁耗时少,但比不需要考虑竞争的单线程耗时多。

回到Disruptor,在我讲生产者时讲过ClaimStrategy。在这些代码中,你可以看见两个策略,一个是SingleThreadedStrategy(单线程策略)另一个是MultiThreadedStrategy(多线程策略)。你可能会有疑问,为什么在只有单个生产者时不用多线程的那个策略?它是否能够处理这种场景?当然可以。但多线程的那个使用了AtomicLong(Java提供的CAS操作),而单线程的使用long,没有锁也没有CAS。这意味着单线程版本会非常快,因为它只有一个生产者,不会产生序号上的冲突。

回到Disruptor,在我讲生产者时讲过ClaimStrategy。在这些代码中,你可以看见两个策略,一个是SingleThreadedStrategy(单线程策略)另一个是MultiThreadedStrategy(多线程策略)。你可能会有疑问,为什么在只有单个生产者时不用多线程的那个策略?它是否能够处理这种场景?当然可以。但多线程的那个使用了AtomicLong(Java提供的CAS操作),而单线程的使用long,没有锁也没有CAS。这意味着单线程版本会非常快,因为它只有一个生产者,不会产生序号上的冲突。

回到Disruptor,在我讲生产者时讲过ClaimStrategy。在这些代码中,你可以看见两个策略,一个是SingleThreadedStrategy(单线程策略)另一个是MultiThreadedStrategy(多线程策略)。你可能会有疑问,为什么在只有单个生产者时不用多线程的那个策略?它是否能够处理这种场景?当然可以。但多线程的那个使用了AtomicLong(Java提供的CAS操作),而单线程的使用long,没有锁也没有CAS。这意味着单线程版本会非常快,因为它只有一个生产者,不会产生序号上的冲突。

这也是为什么Entry中的每一个变量都只能被一个消费者写。它确保了没有写竞争,因此不需要锁或者CAS。

回到为什么队列不能胜任这个工作

因此你可能会有疑问,为什么队列底层用RingBuffer来实现,仍然在性能上无法与 Disruptor 相比。队列和最简单的ring buffer只有两个指针——一个指向队列的头,一个指向队尾:

如果有超过一个生产者想要往队列里放东西,尾指针就将成为一个冲突点,因为有多个线程要更新它。如果有多个消费者,那么头指针就会产生竞争,因为元素被消费之后,需要更新指针,所以不仅有读操作还有写操作了。

等等,我听到你喊冤了!因为我们已经知道这些了,所以队列常常是单生产者和单消费者(或者至少在我们的测试里是)。

如果有超过一个生产者想要往队列里放东西,尾指针就将成为一个冲突点,因为有多个线程要更新它。如果有多个消费者,那么头指针就会产生竞争,因为元素被消费之后,需要更新指针,所以不仅有读操作还有写操作了。

等等,我听到你喊冤了!因为我们已经知道这些了,所以队列常常是单生产者和单消费者(或者至少在我们的测试里是)。

队列需要保存一个关于大小的变量,以便区分队列是空还是满。否则,它需要根据队列中的元素的内容来判断,这样的话,消费一个节点(Entry)后需要做一次写入来清除标记,或者标记节点已经被消费过了。无论采用何种方式实现,在头、尾和大小变量上总是会有很多竞争,或者如果消费操作移除元素时需要使用一个写操作,那元素本身也包含竞争。

基于以上,这三个变量常常在一个cache line里面,有可能导致false sharing。因此,不仅要担心生产者和消费者同时写size变量(或者元素),还要注意由于头指针尾指针在同一位置,当头指针更新时,更新尾指针会导致缓存不命中。这篇文章已经很长了,所以我就不再详述细节了。

这就是我们所说的“分离竞争点问题”或者队列的“合并竞争点问题”。通过将所有的东西都赋予私有的序列号,并且只允许一个消费者写Entry对象中的变量来消除竞争,Disruptor 唯一需要处理访问冲突的地方,是多个生产者写入 Ring Buffer 的场景。

总结

Disruptor相对于传统方式的优点:

没有竞争=没有锁=非常快。
所有访问者都记录自己的序号的实现方式,允许多个生产者与多个消费者共享相同的数据结构。
在每个对象中都能跟踪序列号(ring buffer,claim Strategy,生产者和消费者),加上神奇的cache line padding,就意味着没有为伪共享和非预期的竞争。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值