并行编程中的内存回收Hazard Pointer

接上篇使用RCU技术实现读写线程无锁,在没有GC机制的语言中,要实现Lock free的算法,就免不了要自己处理内存回收的问题。

Hazard Pointer是另一种处理这个问题的算法,而且相比起来不但简单,功能也很强大。锁无关的数据结构与Hazard指针中讲得很好,Wikipedia Hazard pointer也描述得比较清楚,所以我这里就不讲那么细了。

一个简单的实现可以参考我的github haz_ptr.c

原理

基本原理无非也是读线程对指针进行标识,指针(指向的内存)要释放时都会缓存起来延迟到确认没有读线程了才对其真正释放。

<Lock-Free Data Structures with Hazard Pointers>中的描述:

Each reader thread owns a single-writer/multi-reader shared pointer called “hazard pointer.” When a reader thread assigns the address of a map to its hazard pointer, it is basically announcing to other threads (writers), “I am reading this map. You can replace
it if you want, but don’t change its contents and certainly keep your deleteing hands off it.”

关键的结构包括:Hazard pointerThread Free list

Hazard pointer:一个读线程要使用一个指针时,就会创建一个Hazard pointer包装这个指针。一个Hazard pointer会被一个线程写,多个线程读。

struct HazardPointer {
        void *real_ptr; // 包装的指针
        ... // 不同的实现有不同的成员
    };

    void func() {
        HazardPointer *hp = accquire(_real_ptr);
        ... // use _real_ptr
        release(hp);
    }

Thread Free List:每个线程都有一个这样的列表,保存着将要释放的指针列表,这个列表仅对应的线程读写

void defer_free(void *ptr) {
        _free_list.push_back(ptr);
    }

当某个线程要尝试释放Free List中的指针时,例如指针ptr,就检查所有其他线程使用的Hazard pointer,检查是否存在包装了ptr的Hazard pointer,如果没有则说明没有读线程正在使用ptr,可以安全释放ptr

void gc() {
        for(ptr in _free_list) {
            conflict = false
            for (hp in _all_hazard_pointers) {
                if (hp->_real_ptr == ptr) {
                    confilict = true
                    break
                }
            }
            if (!conflict)
                delete ptr
        }
    }

以上,其实就是Hazard Pointer的主要内容。

Hazard Pointer的管理

上面的代码中没有提到_all_hazard_pointersaccquire的具体实现,这就是Hazard Pointer的管理问题。

《锁无关的数据结构与Hazard指针》文中创建了一个Lock free的链表来表示这个全局的Hazard Pointer List。每个Hazard Pointer有一个成员标识其是否可用。这个List中也就保存了已经被使用的Hazard Pointer集合和未被使用的Hazard Pointer集合,当所有Hazard Pointer都被使用时,就会新分配一个加进这个List。当读线程不使用指针时,需要归还Hazard Pointer,直接设置可用成员标识即可。要gc()时,就直接遍历这个List。

要实现一个Lock free的链表,并且仅需要实现头插入,还是非常简单的。本身Hazard Pointer标识某个指针时,都是用了后立即标识,所以这个实现直接支持了动态线程,支持线程的挂起等。

nbds项目中也有一个Hazard Pointer的实现,相对要弱一点。它为每个线程都设置了自己的Hazard Pointer池,写线程要释放指针时,就访问所有其他线程的Hazard Pointer池。

typedef struct haz_local {
        // Free List
        pending_t *pending; // to be freed
        int pending_size;
        int pending_count;

        // Hazard Pointer 池,动态和静态两种
        haz_t static_haz[STATIC_HAZ_PER_THREAD];

        haz_t **dynamic;
        int dynamic_size;
        int dynamic_count;

    } __attribute__ ((aligned(CACHE_LINE_SIZE))) haz_local_t;

    static haz_local_t haz_local_[MAX_NUM_THREADS] = {};

每个线程当然就涉及到haz_local_索引(ID)的分配,就像使用RCU技术实现读写线程无锁中的一样。这个实现为了支持线程动态创建,就需要一套线程ID的重用机制,相对复杂多了。

附录

最后,附上一些并行编程中的一些概念。

Lock Free & Wait Free

常常看到Lock FreeWait Free的概念,这些概念用于衡量一个系统或者说一段代码的并行级别,并行级别可参考并行编程——并发级别。总之Wait Free是一个比Lock Free更牛逼的级别。

我自己的理解,例如《锁无关的数据结构与Hazard指针》中实现的Hazard Pointer链表就可以说是Lock Free的,注意它在插入新元素到链表头时,因为使用CAS,总免不了一个busy loop,有这个特征的情况下就算是Lock Free,虽然没锁,但某个线程的执行情况也受其他线程的影响。

相对而言,Wait Free则是每个线程的执行都是独立的,例如《锁无关的数据结构与Hazard指针》中的Scan函数。“每个线程的执行时间都不依赖于其它任何线程的行为”

锁无关(Lock-Free)意味着系统中总存在某个线程能够得以继续执行;而等待无关(Wait-Free)则是一个更强的条件,它意味着所有线程都能往下进行。

ABA问题

在实现Lock Free算法的过程中,总是要使用CAS原语的,而CAS就会带来ABA问题。

在进行CAS操作的时候,因为在更改V之前,CAS主要询问“V的值是否仍然为A”,所以在第一次读取V之后以及对V执行CAS操作之前,如果将值从A改为B,然后再改回A,会使基于CAS的算法混乱。在这种情况下,CAS操作会成功。这类问题称为ABA问题。

Wiki Hazard Pointer提到了一个ABA问题的好例子:在一个Lock free的栈实现中,现在要出栈,栈里的元素是[A, B, C]head指向栈顶,那么就有compare_and_swap(target=&head, newvalue=B, expected=A)。但是在这个操作中,其他线程把A
B都出栈,且删除了B,又把A压入栈中,即[A, C]。那么前一个线程的compare_and_swap能够成功,此时head指向了一个已经被删除的B。stackoverflow上也有个例子
Real-world examples for ABA in multithreading

对于CAS产生的这个ABA问题,通常的解决方案是采用CAS的一个变种DCAS。DCAS,是对于每一个V增加一个引用的表示修改次数的标记符。对于每个V,如果引用修改了一次,这个计数器就加1。然后再这个变量需要update的时候,就同时检查变量的值和计数器的值。

但也早有人提出DCAS也不是ABA problem 的银弹

原文地址: http://codemacro.com/2015/05/03/hazard-pointer/

written by Kevin Lynx  posted at
http://codemacro.com

时间: 2024-10-12 08:36:34

并行编程中的内存回收Hazard Pointer的相关文章

在 JNI 编程中避免内存泄漏

JAVA 中的内存泄漏 JAVA 编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVM 中 Java Heap 的内存泄漏:JVM 内存中 native memory 的内存泄漏. Java Heap 的内存泄漏 Java 对象存储在 JVM 进程空间中的 Java Heap 中,Java Heap 可以在 JVM 运行过程中动态变化.如果 Java 对象越来越多,占据 Java Heap 的空间也越来越大,JVM 会在运行时扩充 Java Heap 的容量.如果 Java Heap 容量

C#并行编程中的Parallel.Invoke

一.基础知识 并行编程:并行编程是指软件开发的代码,它能在同一时间执行多个计算任务,提高执行效率和性能一种编程方式,属于多线程编程范畴.所以我们在设计过程中一般会将很多任务划分成若干个互相独立子任务,这些任务不考虑互相的依赖和顺序.这样我们就可以使用很好的使用并行编程.但是我们都知道多核处理器的并行设计使用共享内存,如果没有考虑并发问题,就会有很多异常和达不到我们预期的效果.不过还好NET Framework4.0引入了Task Parallel Library(TPL)实现了基于任务设计而不用

并行编程中的取消任务、共享状态,等等

在面对相互独立的数据或者相互独立的任务时,也许正是Parallel登场的时候. 比如说有一个盒子的集合,分别让盒子旋转一定的角度. void RotateBox(IEnumerable<Box> boxes, float degree) { Parallel.ForEach(boxes, box => box.Rotate(degree)); } 如果并行任务中的一个任务出现异常,需要结束出问题的任务呢? Parallel.ForEach为我们提供了一个重载方法,可以控制任务是否继续.

转:【Java并发编程】之十五:并发编程中实现内存可见的两种方法比较:加锁和volatile变量

转载请注明出处:http://blog.csdn.net/ns_code/article/details/17290021 在http://blog.csdn.net/ns_code/article/details/17288243这篇博文中,讲述了通过同步实现内存可见性的方法,在http://blog.csdn.net/ns_code/article/details/17101369这篇博文中,讲述了通过volatile变量实现内存可见性的方法,这里比较下二者的区别. 1.volatile变量

【Java并发编程】之十五:并发编程中实现内存可见的两种方法比较:加锁和volatile变量

在http://blog.csdn.net/ns_code/article/details/17288243这篇博文中,讲述了通过同步实现内存可见性的方法,在http://blog.csdn.net/ns_code/article/details/17101369这篇博文中,讲述了通过volatile变量实现内存可见性的方法,这里比较下二者的区别. 1.volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种

优达学城-并行编程-Unit2 硬件内存

GPU负责给SM分配wrap,SM以并行方式运行程序 在一个SM上跑的所有线程可能合作解决一个子问题(错的,不一定的) 一个单Kernel程序在多个wrap上运行,包含X线程块和Y线程块,可以确定x y先后跑的顺序或是在哪个SM上跑吗? 答:伐晓得(这是cuda的小秘密= =||) GPU的优越性: 1.快速切换SM运行,无法知其间通信 2.可扩展性强,GPU越大,任务分散越广 CUDA存储器类型: 每个线程拥有自己的register and loacal memory; 每个线程块拥有一块sh

分布式异步消息框架构建笔记5——如何避开并行编程中的数据共享陷阱

任何多线程/并行/分布式都会面临一个问题,"数据状态共享". 有经验的开发者会说,要想正确有效的避开避开状态共享,那么就应该别用任何状态共享. 虽然不得不说,这是一个不错的建议,但是没有状态共享,你需要如何才能知道非本地数据的状态? 也许你会说使用消息,使用消息来处理,那么我们丑陋的回调金字塔应该叠的更高了. 不得不说这是一个解决办法,但是为了保持状态不被修改,那么我们还得在远程申请一个写入锁,防止数据被别的任务所修改. 那么流程就是 申请锁->请求某个消息状态->释放锁

.Net中的并行编程-2.ConcurrentStack的实现与分析

在上篇文章<.net中的并行编程-1.基础知识>中列出了在.net进行多核或并行编程中需要的基础知识,今天就来分析在基础知识树中一个比较简单常用的并发数据结构--.net类库中无锁栈的实现. 首先解释一下什么这里“无锁”的相关概念. 所谓无锁其实就是在普通栈的实现方式上使用了原子操作,原子操作的原理就是CPU在系统总线上设置一个信号,当其他线程对同一块内存进行访问时CPU监测到该信号存在会,然后当前线程会等待信号释放后才能对内存进行访问.原子操作都是由操作系统API实现底层由硬件支持,常用的操

C#中的多线程 - 并行编程 z

原文:http://www.albahari.com/threading/part5.aspx 专题:C#中的多线程 1并行编程Permalink 在这一部分,我们讨论 Framework 4.0 加入的多线程 API,它们可以充分利用多核处理器. 并行 LINQ(Parallel LINQ)或称为 PLINQ Parallel类 任务并行(task parallelism)构造 SpinLock 和 SpinWait 这些 API 可以统称为 PFX(Parallel Framework,并行