转载

Lock Free程序中Hazard Pointer的应用

废话不多说了,直接开始讨论。

首先看以下的程序,有问题吗?

#include<iostream> #include<thread> //C++11中的多线程基础设施 using namespace std; int *p = new int(2); void reader() {   if (nullptr != p) { //nullptr是C++11中引入的     cout << *p << endl;   } } void writer() {   delete p;   p = nullptr; } int main() {   thread t1(reader);   thread t2(writer);   t1.join();   t2.join();   return 0; } 

答案当然是有问题。这个程序存在可怕的 race condition :如果当线程 reader 判断完 p 发现它不是 nullptr 后,还未执行第 8 行就被调度出去,轮到线程 writer 执行,它执行完 1314 行后,又继续调度线程 reader ,此时执行第 8 行导致程序崩溃。

这里的本质是对象的生命周期问题,表述为:多线程程序中,某线程通过一个指针访问一段内存时,如何保证指针所指向的内存是有效的?

这当然有多种方法,加一把互斥锁就万事大吉了。不过本文讨论的是 Lock Free 情况下的内存管理,因此我们要讨论的是 2004 年(其实在 2002 年就有人独立提出类似方法)提出的 Hazard Pointer 大法。接着看以下代码:

#include<thread> #include<iostream> #define Strong_Memory_Barrier __sync_synchronize #define ACCESS_ONCE(x) (* (volatile typeof(x) *) &(x)) using namespace std; struct HazardPointer {   HazardPointer() :p_(nullptr){}   int *p_; //Hazard Pointer封装了原始指针 };  int *p = new int(2); //原始指针  HazardPointer *hp = new HazardPointer(); //只有一个线程可以写  class Writer { public:   void retire(int **tmp) {     *retire_list = *tmp;//设置要释放的指针   }   void gc() {     if (*retire_list == hp->p_) {       //说明有读者正在使用它,不能释放     } else {       //可以安全地释放       delete *retire_list;       *retire_list = nullptr;     }   }   void write() {     //....     retire(&p);     gc();   } private:   int **retire_list;//记录待释放的指针 }; class Reader { public:   void acquire(int *tmp) {     hp->p_ = tmp;   }   void release() {     hp->p_ = nullptr;   }   void read() {     int *tmp = nullptr;     do     {       tmp = ACCESS_ONCE(p);     Strong_Memory_Barrier();       acquire(tmp); //封装。这是告诉Writer:我要读,别释放!     } while (tmp != ACCESS_ONCE(p));//仔细想想,为什么这里还要判断?     if (nullptr != tmp) { //再想想,这里为嘛也要判断?       //it is safe to access *tmp from now on       cout << *tmp << endl;//没问题~     }     //when you finish reading it, just release it .     release();//其实就是告诉Writer:用完啦,要释放要保留,悉听尊便。   } }; int main() {   thread t1(&Reader::read, Reader());   thread t2(&Writer::write, Writer());   t1.join();   t2.join();   return 0; } 

总结下:

1,对于读线程,每次访问指针前,都通过 acquire 动作,用 Hazard Pointer 封装一下待读取指针,此举保护了原始指针,向其他线程宣告,我正在读取它指向的内存,请别释放它。用完就 release 一下。

2,对于写线程,真正释放前,需要检查是否有读线程正在操作它。如何知道?看看自己待释放的指针,有没有存在在读线程的 Hazard Pointer 里即可。

至于 52 行,考虑如下情形:读线程刚刚设置了 tmp 指针,还没对它进行保护,就被调度出去;写线程执行 gc 时,发现没有读线程的 Hazard Pointer 封装了 tmp 指针,于是将它释放;等读线程重新被调度执行时通过 tmp 进行内存访问,就会导致问题。

至于 53 行,请读者自行分析。

最后一个问题: Hazard Pointer 的内存空间,谁来负责管理?

在实际程序里,一般所需 Hazard Pointer 的数量不会太多且较为固定,因此基本上只申请,不释放了。

正文到此结束
Loading...