Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 56ac3de

Browse files
committed
2023.02.06
1 parent b212516 commit 56ac3de

File tree

2 files changed

+105
-0
lines changed

2 files changed

+105
-0
lines changed

_posts/2023-02-06-smr-mechanism.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
---
2+
layout: single
3+
title: Differences between SMR mechanisms
4+
date: 2023-01-14 00:00:00 +0800
5+
categories: 论文 实践
6+
tags: C++ Concurrency
7+
---
8+
9+
### Hazard pointer vs shared_ptr
10+
11+
相同之处:管理对象的生命周期
12+
13+
不同之处:一言以蔽之,Hazard pointer是线程安全的,但`shared_ptr`不是
14+
15+
- 为什么`hazard pointer`是线程安全的?
16+
17+
首先读线程不会修改任何指针所指向内容,并且会将指针记录进全局列表中,其他线程是不能对全局列表中的指针所指向的内容进行修改的(包括释放指针所指向的内存)。然后任何写线程,只会释放同时满足以下两个条件的指针所指向内存:
18+
19+
1. 该指针是在当前写线程所替换下来的旧版本指针
20+
2. 该指针没有其他读线程在使用(即没有出现在全局列表中)
21+
22+
读写线程通过这些约束,保证了`hazard pointer`是线程安全的。
23+
24+
- 为什么智能指针不是线程安全的?
25+
26+
不管是`unique_ptr`还是`shared_ptr`都只能管理智能指针中的那个对象的生命周期(而不是智能指针本身),也就是通过引用计数的方式发现如果智能指针中的对象不再被使用,就会回收对应内存。但如果有多个线程同时修改智能指针,将这个指针指向不同对象,又或者其中夹杂若干读取智能指针的操作,这显然不是线程安全的,只能通过外部的锁等机制进行同步。
27+
28+
### Hazard pointers vs Epoch-based reclamation
29+
30+
| | EBR | Hazard pointers |
31+
| --- | --- | --- |
32+
| 核心原理 | 只有全部线程同意,才能推进epoch,过期epoch的指针一定可以安全回收。 | 读线程告知其他线程我正在使用特定指针,其他线程不可以修改或释放这个指针指向的对象,但可以将指针指向其他对象。 |
33+
| 粒度 |||
34+
| 速度 | 快 只需要获取全局epoch即可 | 慢 需要CAS操作一个全局链表 |
35+
| 内存释放内存时机 | 特定epoch没有被任何线程所引用 | thread local列表达到一定长度 |
36+
| 内存释放是否会被阻塞 || 不会 |
37+
38+
这里解释几个方面
39+
40+
- 粒度:EBR一旦进入资源临界区之后,可以操作任意数量和类型的对象指针,只要在退出资源临界区之前,将指针放到epoch对应的列表中即可。但Hazard pointers每次进入资源临界区之前都需要先获取特定类型对象的指针,之后的操作也限定于这个指针,退出资源临界区之前,将指针放到thread_local的列表中。所以从这个角度上说,EBR是粗粒度,而Hazard pointers是细粒度。
41+
- 速度:这里主要对比进入和退出资源临界区的速度
42+
- EBR只需要获取一次全局epoch
43+
44+
```cpp
45+
void acquire() {
46+
active_flags_[get_thread_id()].active_ = true;
47+
CPU_BARRIER();
48+
local_epoches_[get_thread_id()].epoch_ = global_epoch_.epoch_;
49+
}
50+
51+
void release() {
52+
active_flags_[get_thread_id()].active_ = false;
53+
}
54+
```
55+
56+
- Hazard pointers则需要通过CAS操作来在全局链表中获取一个节点
57+
58+
```cpp
59+
HazptrNode* acquire() {
60+
auto p = head_;
61+
while (p != nullptr) {
62+
if (p->active_ || !CAS(&(p->active_), false, true)) {
63+
p = p->next_;
64+
continue;
65+
}
66+
return p;
67+
}
68+
69+
HazptrNode* node = new HazptrNode();
70+
VLOG(2) << "[Node] new node " << node;
71+
node->active_ = true;
72+
73+
HazptrNode* oldHead;
74+
do {
75+
oldHead = head_;
76+
node->next_ = oldHead;
77+
} while (!CAS(&head_, oldHead, node));
78+
return node;
79+
}
80+
81+
void release(HazptrNode* node) {
82+
(node->pHazard_).store(nullptr, std::memory_order_release);
83+
node->active_ = false;
84+
}
85+
```
86+
87+
- 内存释放是否会被阻塞
88+
89+
除了粒度和速度之外,二者最关键的不同之处就在于能否及时回收内存。EBR由于某个线程可能在特定epoch长时间不退出资源临界区,导致内存回收不及时。另外如果大量线程在不断尝试进入和退出资源临界区,也可能导致全局epoch较难推进。所以EBR通常的使用场景在于内存对象的生命周期非常短的情况下。而Hazard pointers则不存在阻塞的问题,任何一个线程所使用的对象,不会阻塞其他线程的内存回收。
90+
91+
### Hazard pointers vs RCU
92+
93+
二者的主要区别同样是在于粒度和是否阻塞。
94+
95+
- 粒度:Hazard pointers如之前所说,只保护单个指针。但RCU是保护可以保护资源临界区中的所有指针指向内存不被释放。
96+
- RCU和EBR一样,如果某个线程在资源临界区的时间过长,会阻止资源临界区中的对象回收。所以RCU对于同样也是期望读临界区的时间足够短。
97+
- RCU可以在读操作时做到wait-free,而Hazard pointers只能做到lock-free
98+
99+
> Quoted from folly:
100+
So roughly: RCU is simple, but an all-or-nothing affair. A single rcu_reader can block all reclamation. Hazptrs will reclaim exactly as much as possible, at the cost of extra work writing traversal code
101+
>
102+
103+
[p0461r1.pdf (open-std.org)](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0461r1.pdf) 这个论文比较了几种延时内存回收的机制
104+
105+
![figure]({{'/archive/SMR.png' | prepend: site.baseurl}})

archive/SMR.png

65.9 KB
Loading

0 commit comments

Comments
 (0)