风险指针(Hazard Pointer) 内存空间共享模型
WiredTiger是一种高性能的开源存储引擎,现已在MongoDB中作为内模式应用。WiredTiger支持行存储、列存储两种存储模式,采用LSM Tree方式进行索引记录
WiredTiger支持事务的ACID特性(原子性、一致性、隔离性、持久性)。对数据的存储方式可采用简易的key/value形式进行存储,也可以使用包含索引映射的数据模式层的方式进行存储。
WiredTiger存储引擎可应用于现代多核CPU架构之上。采用多种实现方式,如风险指针(hazard pointers)、无锁算法(lock-free algorithms)、快锁(fast latching)、消息传递(message passing)等方法。
风险指针(Hazard Pointers)
风险指针是一种内存管理的方法,允许内存被回收后任意重用。该内存管理方法是无等待的(wait-free)。核心操作仅需要单字的内存读取和写入访问。该方法同样提供了ABA问题的一个无锁解决方案。对于指针的管理,通常关注与如何回收已经删除掉的对象所占用的内存。在基于锁的对象的情形下,当线程从对象中删除一个节点时,在它被重用或者重新分配之前,很容易保证没有其他线程会在此后访问该节点的内存。但在无锁动态对象就出现了问题。为了保证无锁的对象的顺利执行,每个线程必须要有无限制的机会在任意时间操作该对象。
内存回收问题是如何允许被删除节点的内存被释放,同时还保证没有线程访问被释放的内存,以及如何以一种无锁的方式做到这一点。之前的用以动态无锁对象的节点重用方法主要有三种:
- IBM标签方法(更新计数)
- 无锁引用计数
- 基于总体引用计数或者每个线程的时间戳方法
风险指针,是一种以无锁对象的内存回收方法。它对每个被删除的节点的分摊时间是预期的常量。无论线程是失败还是延迟,它都能对不可重用的被删除节点的总数保持一个上界。其核心思想是:将一些被称为风险指针(hazard pointer)的单写者多读者的共享指针,与每个想要访问无锁对象的线程关联起来。一个风险指针要么是NULL,要么指向一个节点,该节点之后可能会被该线程访问,而不需要验证对该节点的这个引用是否有效。每个风险指针只能被其拥有者线程写入,但是可以被别的线程读取。
基础模型
基本计算模型是异步共享内存模型。在该模型中一组线程通过在一组共享内存位置上的内存访问操作原语来沟通。线程运行在任意速度,可以有任意延迟。线程对任何其他线程的速度或状态不做任何假设。
共享对象占有一组共享内存位置。对象是一个抽象的对象类型实现的实例,它定义了在对象上可以允许的操作的语义。
原子原语
除了读取和写入外,共享内存位置上的基本操作还包括CAS(compare-and-swap)和load-linked/store-conditional(LL/SC)。
CAS有三个参数:内存位置的地址、预期值、新值。当且仅当存储单元存储的是预期值的时候,新值才能原子地写入该存储单元。该操作返回一个布尔值,用以表示是否发生了写操作。
LL需要一个参数:内存位置的地址,该操作将返回其内容。SS有两个参数:一个内存位置的地址和一个新值。只有当前线程最后使用LL读取该位置之后,没有其他线程写入该内存位置,新值才会原子地写入到该内存位置的地址,并返回一个布尔值,指示自从当前线程最后使用LL读取该位置之后是否有任何其他线程写入过该内存位置。
ABA问题
问题描述如下:它发生在当一个线程从共享位置读取值A,然后其他线程改变该位置为不同的值,例如B。当该线程再次将A放回该共享位置处时,新的线程再次从该位置读取该共享位置处的值时,会误认为该值没有改变,从而错误的执行。
Hazard Pointer要解决的核心问题是如何安全释放内存,该问题解决在实现无锁算法时有两个关键的影响:
- 保证了关键节点的访问是合法的,不会导致程序尝试去读取已经释放了的内存。
- 保证了ABA问题不会出现,程序逻辑正确的前提。
无锁算法中释放内存的难点在于当线程释放了一块内存后,是无法获知是否有别的线程也同时持有该块内存的指针并需要访问。
解决方案
- 建立一个全局数组,数组容量为线程数目,每个线程只能修改自己的数组元素,而不允许修改其他的数组元素,但可以读别的数组元素。
- 当线程尝试访问一个关键数据节点时,先把该节点指针赋给自己的数组元素(即不要释放这个节点)。
- 每个线程自己维护一个私有链表,当线程准备释放掉某个节点时,将该节点放入到链表中。当链表内的数目达到一个设定的数目后,遍历该链表用于释放链表内所有节点。
- 当释放节点时,需要检查全局数组,确定没有任何一个线程的数组元素与当前指针相同时,就释放该节点。否则仍然滞留在自己的链表中。
Hazard Pointer主要应用在实现无锁队列上。
- 队列上的元素任何时候,只可能被其中一个线程成功地从队列上取下来,因此每个线程的链表中的元素肯定是唯一的。
- 线程在操作无锁队列时,任何时候基本只需要处理一个节点,如果有特殊需求,就需要有额外的扩展
- 对于某个节点,多线程同时持有该节点的指针的现象在时间上是非常短暂的。只有当这几个线程同时尝试取下该节点,它们才可能同时持有该节点的指针,一旦某线程成功将节点取下,其它线程很快就会发现,并尝试继续操作下一个节点。