深入解析 Hazard Pointer (上)
废话不多说了,直接开始讨论。
险象环生
首先看以下的程序,有问题吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#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执行,它执行完13、14行后,又继续调度线程reader,此时执行第8行导致程序崩溃。
这里的问题,可以不精确地表述为:多线程程序中,某线程通过一个指针访问一段内存时,如何保证指针所指向的那块内存是有效的?
Hazard Pointer
这当然有多种方法,加一把互斥锁就万事大吉了(当然,得注意锁本身的生命周期)。不过本文讨论的是lock free情况下的内存管理,这里要介绍的是2004年左右提出的Hazard Pointer方法。
接着看以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
#include<thread>
#include<iostream>
#define Memory_Barrier __asm__ __volatile__("" : : : "memory")
#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;
}
}
void write() {
int *tmp = ACCESS_ONCE(p);
p = nullptr;
retire(tmp);
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);
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里即可。
至于55行,考虑如下情形:读线程刚刚设置了tmp指针,还没对它进行保护,就被调度出去;写线程执行gc时,发现没有读线程的Hazard Pointer封装了tmp指针,于是将它释放;等读线程重新被调度执行时通过tmp进行内存访问,就会导致问题。
至于56行,请读者自行分析。
那么,Hazard Pointer和Smart Pointer(智能指针),有什么本质上的区别吗?请看下集。
参考文献
https://www.research.ibm.com/people/m/michael/ieeetpds-2004.pdf