数据库系统中的Latch-free同步

《Latch-free Synchronization In Database System: Silver Bullet or Fool's Gold》
数据库系统中的Latch-free同步是一大利器还是虚有其名?

摘要

随着多核的数据库结构的发展,有一个争论:要不要丢弃Latches改用Latch-free。基于Latch的同步因为用了排他锁给人一种扩展性不强的感觉;而Latch-free在理论上保证了一个线程运行的过程不会因为延迟或其他线程的失败而受阻塞。
本文研究了影响Latch-free和Latch-based同步的性能与扩展性的多种因素。
发现:the argument for latch-free algorithms' superior scalability is far more nuanced than the current state-of-the-art in multi-core database architectures suggests.
Latch-free的扩展性好的优势对比当前最新多核数据库结构是非常微不足道的。

1 介绍

传统数据库广泛的使用latch。Latch保证在一个时间点上只有一个线程能修改共享的数据结构。但是Latch-based的算法被认为其性能会受到规模的影响,一个线程获得latch后延迟了,那么等待这个latch的所有线程在其delay的时间段都无法获得latch。而Latch-free在理论上(至少)保证了不会有线程会由于delay或者其他线程的失败而阻塞——因此一些研究就得出了Latch-free比latch-based扩展性更好或者Latch-free胜过latch-based的结论。

本文认为Latch-free的理论保证与其性能和扩展性是无关的在多核硬件上。影响一个同步机制的扩展性最重要的因素是——在全局内存地址上避免冲突的能力,而与是不是基于Latch无关。实验证明,性能问题不是因为有没有用latch,而是与资源分配效率有关。

Latch-free经常被忽略的一个点就是它通常都需要特殊的内存管理机制。Latch-free相对于latch-based的内存管理机制增加了复杂性和开销。实验发现,Latch-free优于最简单的busy-waiting latch,与更优的backoff latch互有胜负,但不超过scalable queuing latch。
(busy-waiting latch就是test-and-set,backoff latch是获取锁失败的线程可能sleep之类的,scalable queuing latch就是MCS)
本文表明latch-free的优秀的扩展性实际上比一些论文讲的小,因为他们都是和效率低的粗粒度的latch进行的比较。 重点讨论影响latch-free的多种因素

2 Latch-free

2.1可扩展性

2.1.1同步性能

(线程需要同步机制访问共享数据结构)
Latch通常用一个word,其值表明有没有被线程持有。在latch-free中,线程用原子指令保证正确的更新或者读取共享数据结构。两类方法的性能都与在内存中对一个word的并发和写的性能有关。

在多核硬件上,有两条经验法则决定在特定内存位置的并发读和写的性能:1.原子指令,CaS和xchgq,它们写(或尝试写)内存中特定的字节上是串行执行的。因此如果几个cores并发地尝试对内存中特定的字节进行更新,处理这些指令的时间就是就和要写的cores的数目成比例;2.最新写到内存中的值要串行传播到其他读core上——这也就是说,一个core写到内存中一个值,有些core要读这个值,新值传播到其他读core的时间是与读core的数目成比例的。

一些latch的实现是在争用下是不可扩展的因为他们不能很好的满足上面提到的多核硬件的特性。latch最简单的实现是test-and-set(三个参数,地址、原始值、修改值,去地址中取出,与原始值进行比较,如果相同写入修改值,不同失败。),cores会不断重复test-and-set把内存中一个word的值从0改为1,test-and-set首先会无条件的进行修改,然后返回原来的值与其参数中的原始值进行比较,如果相等表示修改成功,获得latch;如果不等,表示别的core正持有latch。在tight loop中不断进行test-and-set会给内存控制器很大的压力,在跨NUMA上也会增加traffic。这会影响到那些不参与test-and-set的core无冲突的线程。并且在释放latch的时候,需要把latch从1改为0,这又要和要请求这个latch的线程进行test-and-set竞争,latch释放的延迟又增大了the length of critical section。

为解决上面的问题,一个方法是先读latch的值,只有是0的时候才进行test-and-set,叫作test-and-test-and-set TATAS。在别的线程持有latch的时候本地缓存的副本上自旋,没有给内存控制器和NUMA interconnecttion压力,释放Latch的时候也不会和想要获得latch的test-and-set竞争。但是这也只不过减少了争用部分的长度,在criticla section较小的时候,其性能主要受到latch转移的影响,c1释放latch, c2...5都在等待latch,这时候c2知道latch的值发生改变开始test-and-set获取了这个latch,但是c2获得latch的消息还没有通知到其他core,其他core也开始了不会成功test-and-set,又出现了原始的矛盾。

test-and-set和TATAS的问题都是在一个全局的内存地址上的自旋。

防止在全局自旋的两种方法:1.backoff机制,每个原子修改或读指令都用noop指令分开;另一种backoff依靠系统调度线程。

可扩展的latch数据结构其线程在Threadlocal或core local上自旋,如MCS。MCS构建了一个等待获取latch的队列,每个线程都在队列上有一个相应的节点,通过xchgq指令原子加入队列。如果其对应的节点在队列上不是第一个的时候那么自旋(线程都在各自的节点上的标志自旋)。当latch的持有者完成操作后,将下一个队列节点的falg设为true。——MCS 线程不是在共享内存地址上自旋,而是在各自的节点flag上。传统的锁存实现允许多个线程在一个循环中写入一个全局内存位置(通过测试和设置),并且由于这些test-and-set请求是序列化的,因此速度会减慢,以及诱导缓存一致性traffic使读取锁存字值的核心上的缓存线失效并重新加载.

而Latch-free,线程用原子指令保证写的原子性。线程会尝试性地读共享状态,根据读的结果进行计算然后尝试性进行原子提交。在线程进行尝试性读和原子性更新的过程中,可能有其他线程因为冲突的更新而导致其读取无效。线程通常用CaS指令进行验证共享状态有没有改变。(比如线程t1在时间1开始读,在5进行了更新,线程t2在时间2开始读,在6开始更新,t2在CaS更新的时候失败)

尝试性读的Latch-free与latch有一样的扩展性瓶颈。如果总是因为冲突更新失败,线程仍然会重复尝试操作。重复尝试CaS指令是串行化的,会导致减速因为这些重复都是成功speculate的线程和不成功的线程执行的。 结果就是speculative operation的冲突窗口变大。

由于Latch-free的乐观的天然属性和乐观的代价(如复制的开销),很多人会想着把speculative latch-free和no-speculative Latch-based的区别与乐观和悲观并发控制联系起来。然而本文认为两者的相同之处要远多于不同之处,因为两者都是要在一个共享内存地址上进行更新,都受支配与same underlying hardware performance characteristics。而乐观与悲观并发控制的不同时由于他们的冲突解决机制——悲观在另一个冲突事务执行时阻塞这个事务,乐观在检查到冲突时abort事务。,两者是有一组不同的折中方案来管理的,乐观在高争用下浪费满载系统的资源,悲观在低竞争低负载系统中有不需要的保守调度。

可扩展的Latch-based技术不能直接用于latch-free的实现,latch-based技术能够很好的确定不同的线程获取latch的顺序,一个线程确定优先级后delay,优先级较低的线程都会delay。而latch-free的理论就要保证一个线程的delay不会影响其他线程,因此预先确定顺序的技术与latch-free是不兼容的,最多只能用于辅助的竞争处理机制;使用预先确定的线程优先级的latch-free必须能够按照预先确定的顺序“超时”,fallback到用CaS。

2.1.2调度请求

DBS用multi-programming levels MPLs的概念来决定能够同时执行的请求数量,也就是DB worker。DBMS有不同的进程模型,讲worker与进程或者线程进行映射。

如果worker超过了core数量,也就是至少有一个core执行了多个worker。OS调度OS contexts, 给每个contexts固定的时间片,时间片结束换下一个contexts。而OS经常不知道一个contexts有没有获得latch——一个Context可能获得了latch但是时间片用完了换另一个Context。

大多DBMS都给每个worker一个单独的OS context(创建一个新的context或者有一个context pool)。而MPLs通常要比CPU core数量多,大多数DBMS的worker和context都是1:1,也就是worker要比CPU cores多得多。这可能导致thrashing由于latch的持有者被抢占执行。

在传统数据库系统架构中,latch-free的progress guarantees是非常有价值的,因为抢占的Context不会阻塞或delay其他context的执行。但是对于lath-based就不是这样了,core1中的T1,core2中的T2和core3中的T3产生冲突,此时T1持有latch,但是core1中有两个线程轮替执行,会delay T2和T3。

但是,导致抢占的根本原因不在于是不是用了latch,而是DBMS用了比core多的OS context, 并且没有足够的user-level同步的信息来调度context。 数据库本身可以在用户空间实现一个调度机制不依赖系统支持。这个调度机制保证不会超过core数量的context,一些内存数据库已经在用这些process model了。并且DBMS有很长的传统实现用户级的context调度(通过DBMS 线程),在OS多进程不支持或者效率低的环境中。

2.2内存管理

latch-based不允许在一个线程访问一个数据结构时其他的冲突线程并发访问这个数据结构。但是latch-free不能限制线程访问访问数据结构因为如果任何冲突线程delay了受限制的线程都无法前进,这与latch-free的严格的进度保证是相冲突的。

2.2.1复制开销

latch-based的更新导致的不一致的中间状态是安全的,因为排他锁不允许其他线程访问,latch-based的更新也总是就地更新。

latch-free为了保证线程在不考虑是否存在冲突的情况下取得进展,数据结构的状态必须总是一致的。为了更新这个复杂的数据结构,线程必须复制一个数据结构(或者它的一部分),并对这个本地副本进行更新。这个更新通过一个原子指令对其他线程可见。

因此在复杂的数据结构上进行更新的latch-free必须有复制部分数据结构的额外开销。并且——如果latch-free是speculative的,复制数据结构的开销会延长冲突窗口的持续时间,在此期间更有可能失败,最后在副本上操作的cache利用率更差。

2.2.2 垃圾收集GC

在latch-free中,一个对象被线程删除可能有其他线程仍在访问,因此不能立即删除。通常做法是延迟内存回收,比如hazard pointers和 epoch-based 回收。
延迟内存回收两个问题:

  • 为了确定何时能够安全回收需要额外的开销,epoch-based开销很小
  • 更重要的是,延迟回收不能像传统内存分配器一样的用作黑匣子,latch-free的内存分配逻辑与latch-free是紧密相连的。

2.2.3 内存重用 memory reuse

latch-free中线程用CaS指令来保证正确性,但它可能错过其他线程的并发更新。
线程T从内存地址a读到了值A,线程T'把值从A改成了B又改成了A。如果T要用CaS根据之前的A进行修改(CaS里面的参数就是a, A, C(要修改成))仍会成功,尽管其值发生了两次变化ABA。ABA问题可能会造成一些不易察觉的bug 如果正确性是以(在T读和后来的CaS的过程中不能有干涉更新)为标准。

ABA问题通常表现在基于指针的数据结构如链表和队列的latch-free中。如在一个latch-free的排序链表上。在链表上插入一个值为7的节点N7,先遍历链表发现N7应该插入到N5和N9之间。插入线程先把N7的向后指针设置为N9,然后用CaS在验证N5的向后指针是N9并将其设置为N7。如果N5被删除掉了然后插入了一个值8,那么N7 CaS仍是成立的因为这个N8指向N9。但是这样的话,这个链表就不在有序了。

会发生上面的情况是因为N5这个节点在N7读之后和CaS期间被reuse了。要解决ABA问题,
需要保证释放内存后不立即被其他线程使用。垃圾收集是确保不过早释放内存,而ABA问题是确保释放出的内存不会太早reuse。

2.3 复杂度

Latch-free详细说起来非常复杂,更不用说实现了。一些大师的设计也出现了错误。

2.3.1 模块化

一些人说latch-free能简化系统设计,因为操作系统的中断处理程序代码和非中断处理程序代码获得相同的latch会产生死锁,而Latch-free不会死锁,所以一些研究人员提出使用latch-free简化中断处理程序代码。
但是数据库系统线程和进程不需要处理中断,latch-free不能为数据库提供与操作系统内核相同的模块化好处。

2.4 讨论

本节讨论了latch-free的局限和开销, 但确实有这样的情况:latch-free比latch-based提供更好的扩展性,某些latch-free允许多个线程同时修改。
一般来说,开发者和架构师应该确定他们的算法和多核硬件的性能特征如何交互。简单的把latch-based转换成latch-free很少成功。
有一些把latch和无同步操作产生了很好的效果,如read-copy-update技术(广泛用于linux内核)和MassTree,不为读更新元数据(因为如果是读写锁的情况,读也需要往里面写东西,对于多核来说,一个core写了东西,其他core的cache Line就失效了),写用latch。read optimized write exclusion也是吧

3 案例研究:基于树的索引

在任何时间点,多个插入、查找和删除请求都可以针对一个索引并发执行,因此索引必须支持同步机制来正确的排序并发请求。
updater和reader都要从根节点开始访问,B+树又是层次树,上层的同步很难。

3.1 Contention for logical locks

B+树上最有名的同步并发就是latch coupling了。从根节点开始,请求获取节点的latch,然后确定接下来的子节点,再去请求latch,递归进行。当孩子节点是安全的,释放其祖先节点所有latch。对于查找,从上到下获取读锁,读到孩子释放父亲节点的latch;对与插入与删除,从根节点开始向下,获取写锁,如果孩子节点安全释放祖先节点的所有锁。
实际上更复杂:查询要获取意向共享锁IS,读到孩子节点释放父亲节点的锁;更新(插入与删除)要获取共享意向排他锁SIX,直到安全节点。向一个满的节点插入,它要分裂成两个节点,又会向其父节点插入一个有可能导致父节点的分裂等等;删除操作类似。更新操作需要把SIX转化成X排它锁,受到孩子节点更新影响的中间节点都要加。
SIX与IS是兼容的,但和其他SIX不兼容。也就是updater允许reader同时读,但不允许其他updater访问这个节点。这是悲观的策略,因为updater要获取SIX,不允许其他updater访问,尽管加了SIX的节点不一定会修改。注意:由于B+树分层的组织结构,一个中间节点由于孩子节点的插入与删除而被修改的概率从叶子节点到根节点是指数级下降的。因此更高的内部节点如根节点的SIX会保持很短的时间,通常只有在updater检查孩子节点是不是满的的时候。尽管如此,在根节点加SIX也会严重影响B+树的性能。

之后的B+树为了避免在根节点的阻塞,哪怕是短时间的,updater用的是和查找一样的IS或S锁,在到达叶子节点找到需要update的节点后,updater从叶子节点到其更高的节点进行修改。 基于corrctly interleave tree descent和自底向上修改把这些算法分为两类:ARIES/IM是乐观并发控制,下降请求和自底向上修改;Blink树是在内部节点维护额外的信息,以便读总是能够正确导航树。

3.2 在共享内存上的争用(主要是指写锁或Counter之类的)

在当前多核硬件上,读锁的获取也是不可忽略的开销。即使是请求相同的兼容的latch,每个操作对共享的内部锁的元数据进行修改(比如记录reader数量的Counter)。因为所有的操作都要从根节点开始,内部的元数据每个操作都要更新。当多个请求同时尝试遍历树,那么更新根节点的Counter的时间与并发请求的数量成正比。现代B+树索引为了避免这种频繁的同步,用的是自底向上的修改(基于Blink树和ARIES/IM)

Blink树上一种乐观读写同步机制OLFIT。获取排它锁以更新节点并增加这个节点的版本号;读需要等待latch的释放,读版本号,然后再验证版本号有没有发生改变。——这种方法就不用加写锁了(写到共享内存地址上)。上层节点被修改的概率比叶子节点指数级别的低,OLFIT解决了在根节点上频繁的同步。这种基于时间戳的验证是现在一些系统的基础,如MassTree, Silo。

实现评估

Microbenchmarks

本节将比较Latch-free和三类基于latch的方法TATAS(test-and-set)、Pthread mutexes(backoff latches,遇到冲突线程sleep)、MCS。线程的执行分为两个阶段串行阶段和并行阶段。
两类方法在串行阶段机制不同,latch-based是线程只有相应的latch才能执行,而latch-free是线程在执行串行前要先读Counter的值,推测性的执行串行阶段,然后尝试以原子的CaS对旧的Counter进行递增。如果CaS成功,成功执行,如果CaS失败,线程会再次读Counter尝试重新执行串行阶段。对此的一个优化可以减少CaS的次数,就是在CaS前先检查值是否不变。
下面是在80 cores的机器上运行

低冲突

0.05%



在线程数少于核心数的时候没有明显差异。但线程数多于核心数的时候,MCS性能迅速降低,这是因为MCS给每个latch有一个等待线程队列,一个线程优先,就算没有持有,队列后面的也必须等待直到这个线程被再次调度完成才能进行,也就是说后面所有的线程都要delay(也是因为因为一个core上有多个线程,拥有latch的线程无法持续执行,因为是按顺序执行的); TATAS 线程的抢先不会影响其他线程。 Pthread 获取latch失败的线程在kernel中sleep了,那么就只能全力执行那些获取latch的线程了。
而Latch-free不受线程数增加的影响,性能不减少是因为线程的preemption完全不影响其他线程;不增加是因为所有的core已经完全利用了,再增加线程也不会增加throughput。

中冲突

0.5%


TATAS 40个线程后性能急剧下降:这是因为在latch在切换所有者的时候cores会执行大量的xchgq指令,这些xchgq指令又会影响其他持有latch的指令释放latch,增加了串行阶段。并且随着cores的增加,会有更多的xchgq。

Latch-free在50线程后性能下降:和TATAS类似,线程在执行前读counter,最后用CaS验证,如果多个线程同时尝试CaS,将只有一个线程成功。另外也会有一些线程(在counter已经变化的情况下)仍要CaS,这些添乱的CaS又会推迟后面指令的执行,因为在同一块内存上的原子指令的执行是顺序的。和TATAS一样这也会增加串行执行的长度。

Pthread在吞吐到达顶峰后没有下降:Pthread有一个内在的冲突处理机制,获取Latch需要两次成功的CaS,失败则在linux kernel中sleep。Serial Phase Latency显示75%的线程不会backing off,25%的线程要在Kernel中sleep。没有backoff情况下执行串行阶段延迟的变化是由CaS的竞争引起的

MCS:在线程不超过核心数的时候,MCS的可扩展性非常好。获取latch时,线程执行一个xchgq,然后在自己的local cache line上自旋,避免了CaS和Cache失效的开销。

高冲突

5%



MSC更可靠,是因为MCS在执行串行阶段之前会确定线程的优先级。延迟趋势对并发有重要意义。

4.2 Queuing experiment

本节展示了防止重复更新一个共享内存地址的影响在一个正确的示范上。这里建立了两个并发队列,一个是有latch的,一个是latch-free的。latch-based的队列用MSC来确定线程的优先级;一个是Bw树节点的用delta linked-list更新的节点。




(Line2是把要插入的节点qnode的前指针设为空,充当latch,然后用xchgq把tail的向后指针指向qnode,返回tial,再把qnode的前指针指向tail)
两种都很好理解
实验表明,在高冲突下,latch-based比latch-free的快三倍,原因在于latch-free成功和不成功的CaS都要在tail执行。两种不成功的CaS,一种是几个线程都在读尾节点,要插自己的节点,只有一个能成功;另一种是硬件delay导致在tail的值上发生改变,导致这个CaS就不可能成功。
在latch-based xchgq算法中,线程需要等待新插入节点的prev从invalid变成valid,但是这个开销很小,因为这个prev立刻就被后面的指令更新了。更重要的是,这个prev只能由它的插入进程更新,不会发生冲突。

在低冲突下,两者性能一致。

这个例子展示了设计同步机制的时候减少在一个共享内存地址上重复的重要性。

5启发

在硬件水平上,能影响一个并发机制可扩展性的唯一要素就是减少在共享内存特定地址重复的读和写。
实现上可以用MCS在不同的位置自旋,或用backoff机制(Pthread latch获取latch失败的线程sleep)
另外也发现,当OS over-subscribed(应该是线程数超过core数的意思),抢先调度会影响用户空间同步机制的性能。
基于以上观察,列出了未来多核数据库系统研究的avenues。

5.1上下文调度机制

用户级调度机制的限制是由于上下文调度软件不知道用户级调度,如OS和hypervisor。如在4的试验中,MCS和TATAS的latch都是由user-level mechanisms构建的,当持有latch的线程被抢占后性能很差因为线程数超过了可用的core数。这不是数据库独有的问题。
协作调度
windows支持User-Mode Scheduling

5.2 消息传递

任何基于共享内存通信的并发变成模型都不能避免同步。线程要在数据进行冲突的修改,需要某种形式的同步,latch或者latch-free,以防止数据结构因为竞争而出现不一致。

作为共享内存的替代方案,线程间可以显式传递消息。基本思想是避免在多个线程上共享状态,相反,每个线程都保持本地状态,并且只有该线程才被允许读取或更新该状态。显式消息传递可以避免与共享内存通信相关的同步开销。共享内存同步的问题是,随着核心数量的增加,其开销会变得非常大。另一方面,消息传递显式地限制了同步开销。

消息传递的一个缺点就是在core上消息排队导致延迟。

消息传递与共享内存通信之间有中间地带。如可以把共享内存交互限制在单个的NUMA socket执行的线程上,而在跨socket通信用显式消息传递。

5.3 Advanced planning

系统可以预先确定valid调度执行操作,这会减少同步时的开销,因为已经有了明确的计划。在更高的层次上,预先确定的调度可以把操作分为若干个集合,这样在不同的集合间的操作就不会发生冲突了。可以同时执行不同集合中的操作,不需要任何同步,如PALM-Tree。PALM tree积累成批的索引操作,然后对每个batch中的操作进行分析,然后把这些操作划分到不冲突的结合,然后指派一个线程在特定的集合上进行操作。

advanced planning带来的好处有多大取决于两方面:

  • 它是在扩展性和延迟之间的取舍。它通过减少同步来提高可扩展性,但是它把操作挂起又会增加延迟。延迟的增加可能影响整个系统的性能。如在B+树中,索引操作的延迟可能会使相应的数据项的latch保持更长的时间,可能会增加乐观验证的错误。因此advanced planning通常需要端到端地了解延迟增加对其他无关组件或者更高级别概念的影响。
  • 其次,为了构建具有并行执行机会的进度计划,高级计划要求操作之间的冲突可以推断为其执行。然而,应当指出的是,行动之间的冲突不一定总是如此。advanced planning只能将并行工作分配给数十个或数百个物理CPU。因此,不冲突的操作集的数量不一定要很大。

上面的多核事务处理机制是显式设计来解决这两个限制的。首先,为了避免延迟增加带来的有害影响,为其执行创建事务调度。创建计划所涉及的额外延迟不会增加由于冲突而阻止冲突事务的持续时间。相反,它允许构造积极的可串行化事务调度,从而允许冲突事务之间的并发性显著高于相应的最新并发控制协议。第二,为了确定事务是否冲突,这些事务处理机制会推测性地执行事务逻辑的一个子集,或者在分区甚至整个表的粒度上使用粗粒度的冲突信息,这些信息可以通过对事务的静态分析获得

5.4异步协调

避免同步的方法就是不需要同步。分布式系统中大量的工作都是为了避免同步。

例如Silo的恢复协议的物理redo阶段。物理redo日志记录了相应的数据库对象,Silo给物理redo日志记录一个连续数字表示的时间戳。如果一个page上多条record都要redo,最新的赢,这里用了 last-writer-win的规则——不管日志记录处理的顺序如何,特定对象的状态都能恢复。

即使不能用异步协调直接实现,也可以用特定领域的知识在受限的情况下使用异步协调。一个例子就是linux内核中的read-copy-update(RCU)(对一个RCU保护的共享数据结构,读操作不用锁就可以了访问,但写的时候是先拷贝一个副本,对副本进行修改,最后在适当的时候把原来的数据指针重新指向这个被修改的数据,这个时机就是所有引用该数据的CPU或者说线程都退出对这个共享数据操作的时候)RCU用上下文切换 信息来决定删除的对象有没有引用。如果一个对象被一个特殊的线程删除了,RCU允许并发线程持有这个对象的引用,会延迟释放内存,直到没有线程引用。一个被删除对象内存的回收至少要经过CPU切换一次上下文。 RCU就消除了昂贵的track引用一个对应的线程,比如用counter。维护一个counter很贵,它需要频繁的写。

6结论

当操作系统上下文数量超过core数量时,latch-free通常(不是总是)超过latch-based。然而随着现代数据库系统,尤其是内存数据库系统——都在向 OS上下文与处理核心一对一的 处理模型转变。这就导致latch-free被边缘化了。并且它和latch-based一样受支配于同步开销。这样的环境中Latch-free从来没有超过过latch-based。此外,latch-free还需要特殊的内存管理机制和其他的复杂性,这些在latch-based中是不存在的。因此,本文警告不要在没有仔细研究过latch-based替代方案前就实现latch-free。
最后强调,可扩展性多核数据库应该致力于防止在共享内存的同一个地址上频繁同步。系统 可扩展性取决于避免这种频繁同步的能力,而不是用不用latch。

你可能感兴趣的:(数据库系统中的Latch-free同步)