风险指针(hazard pointer)
前言
无锁(Lock-free)对象比传统的基于锁的对象提供了显著的性能和可靠性等优点。然而,由于缺乏一个有效可移植的无锁方法来回收这些对象中删除掉的动态节点所占用的内存,成为了在实践中广泛应用该方法的一个主要障碍。风险指针是一种内存管理的方法,允许内存被回收后任意重用。该内存管理方法是无等待的(wait-free)。核心操作仅需要单字的内存读取和写入访问。该方法同样提供了ABA问题的一个无锁解决方案。对于指针的管理,通常关注与如何回收已经删除掉的对象所占用的内存。在基于锁的对象的情形下,当线程从对象中删除一个节点时,在它被重用或者重新分配之前,很容易保证没有其他线程会在此后访问该节点的内存。但在无锁动态对象就出现了问题。为了保证无锁的对象的顺利执行,每个线程必须要有无限制的机会在任意时间操作该对象。
核心思想
核心思想是,将一些(典型地是1或2个)被称为风险指针(hazard pointers)的单写者多读者(single-writer multi-reader)的共享指针,与每一个想要访问无锁对象的线程关联起来。一个风险指针要么是NULL,要么指向一个节点,该节点之后可能会被该线程访问,而不需要验证对该节点的这个引用是否有效。每个风险指针只能被其拥有者线程写入,但是可以被别的线程读取。
该方法要求无锁算法保证,当动态节点可能已经被从对象中删除掉的时候,没有任何线程可以访问该动态节点,除非该线程的至少一个关联的风险指针从该节点还可以保证能够从对象的根到达开始已经连续指向该节点。该方法防止释放任何已经在其被删除之前已经被一个或多个线程的一个或者多个风险指针指向的节点。
每当一个线程退休一个节点,它就将其保持在一个私有列表中。在累积了一定数量R个退休节点之后,该线程就扫描其它线程的风险指针,寻找与累积的节点地址的匹配。如果退休节点没有与任何风险指针匹配,那么释放该节点就是安全的。否则,该线程继续保持该节点,直到它下次扫描这些风险指针。
解决方案
建立一个全局数组,数组容量为线程数目,每个线程只能修改自己的数组元素,而不允许修改其他的数组元素,但可以读别的数组元素。
当线程尝试访问一个关键数据节点时,先把该节点指针赋给自己的数组元素(即不要释放这个节点)。
每个线程自己维护一个私有链表,当线程准备释放掉某个节点时,将该节点放入到链表中。当链表内的数目达到一个设定的数目后,遍历该链表用于释放链表内所有节点。
当释放节点时,需要检查全局数组,确定没有任何一个线程的数组元素与当前指针相同时,就释放该节点。否则仍然滞留在自己的链表中。
主要用于比对自己的私有链表存储的节点是否存在于全局数组中,如果出现在全局数组中,表示正有线程访问私有链表中的节点,则不能删除。
Hazard Pointer主要应用在实现无锁队列上。队列上的元素任何时候,只可能被其中一个线程成功地从队列上取下来,因此每个线程的链表中的元素肯定是唯一的。
相关术语
节点(Node):我们使用术语节点(node)来描述一个内存位置范围,这个内存位置范围在某些时候可以被看作是一个逻辑实体,要么通过其在使用了风险指针(hazard pointers)的对象中的实际使用,要么通过参与线程的视角。因此,可能会有多个节点物理上重叠,但仍
然、被看作是不同的逻辑实体。在任一时刻t,每个节点n处在下列状态之一:
1. 已分配(Allocated): n已经被一个参与线程分配,但尚未插入一个相关联的对象。
2. 可到达(Reachable):n是可以通过从根部开始跟踪一个相关联的对象的有效指针是可达的。
3. 已删除(Removed):n不再可到达,但仍可能在删除线程(removing thread)中正被使用。
4. 已退休(Retired): n已经被删除并被删除线程用完,但尚未被释放。
5. 空闲(Free):n的内存可以被分配使用。
6. 不可用(Unavailable):n的部分或全部内存被一个不相干的对象在使用。
7. 未定义(Undefined):n的内存访问目前没有被当作一个节点看待。
拥有(Own):一个线程j在时刻t拥有一个节点,当且仅当在时刻t节点n对于线程j 属于allocated, removed, 或者retired状态。每个节点都可以有最多一个拥有者。被分配的(allocated)节点的拥有者(owner)是分配它的线程(例如,通过调用malloc)。被删除的(removed)节点的拥有者(owner)是执行将它从对象中删除步骤的线程(也即,从将其状态从可到达(reachable)改为已删除(removed))。已退休(retired)节点的拥有者(owner)与删除它的线程是同一个线程。
安全的(Safe):一个节点n对于线程j在时间t是安全的,当且仅当在时间t,要么n是可达的,要么j拥有n。
可能是不安全的(Possibly unsafe):一个节点从线程j的视角中在时间t可能是不安全的(possibly unsafe),如果不可能仅仅通过检查j的私有变量以及该算法的语义,就可以确定对于j在时间t该节点绝对肯定是安全的。
访问风险(Access hazard):在线程j的算法的一个步骤是一个访问风险(Access hazard),当且仅当它可能会导致访问到对于线程j在其执行的时间内可能是不安全的(possibly unsafe)节点。
ABA风险(ABA hazard):线程j的算法中的一个步骤s是一个ABA风险(ABA hazard),当且仅当线程j在执行s时在一个可能不安全的(possiblyunsafe)动态节点上包括了容易发生ABA问题的比较(ABA-prone comparison)操作,这样,1)节点的地址,或它的一个算术变体,是容易发生ABA问题的比较(ABA-prone comparison)操作的一个预期值,或2)包含在动态节点中的内存位置是容易发生ABA问题的比较(ABA-prone comparison)操作的目标。
有访问风险的引用(Access-hazardous reference):线程j在时刻t包含对节点n的一个有访问风险的引用(accesshazardous reference),当且仅当在时刻t,j的一个或者多个私有变量持有n的地址,或者其算数变体,并且j可以保证(除非它崩溃了)能够到达访问风险(access hazard)s,在那里有风险地使用n的地址,也即,在n对于j可能是不安全的(possibly unsafe)时候访问n。【译注:真TNND的绕啊,基本意思就是,存在一个引用,肯定能到达,但是一旦访问就有风险!】
有ABA风险的引用(ABA-hazardous reference):线程j在时刻t包含对节点n的一个有ABA风险的引用(ABA-hazardousreference),当且仅当在时刻t,j的一个或者多个私有变量持有n的地址,或者其算数变体,并且j可以保证(除非它崩溃了)能够到达ABA风险(ABA hazard)s,在那里有风险地使用n的地址。
有风险的引用(Hazardous reference):一个引用是有风险的,如果它有访问风险(access-hazardous),和/或者有ABA风险(ABA-hazardous)。
非正式地,有风险的引用是一个地址,不经进一步的安全性验证,之后会以存在风险的方式被使用,即,在容易发生ABA问题的比较(ABA-prone comparison)操作中访问可能不安全的存储器,和/或作为目标地址,或者预期值。