假设没有版本号,两个线程同时操作一个链表可能会发生什么?
没有版本号的 C++ 实现:
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
| // 链表节点定义
typedef struct Node{
struct Node* next;
int val;
} Node;
class LinkList {
std::atomic<Node*> head;
Node* pop(){
Node* oldHead = head.load();
Node* newHead = nullptr;
do{
if(oldHead == nullptr) return nullptr;
newHead = oldHead->next; // 有崩溃风险,考虑如何用 shared_ptr 来解决
} while(head.compare_exchange_weak(oldHead, newHead) == false);
return oldHead;
}
void push(int val){
Node* newNode = new Node{nullptr, val};
Node* oldHead = head.load();
Node* newHead = newNode;
do{
newNode->next = oldHead;
}while(head.compare_exchange_weak(oldHead, newHead) == false);
}
};
|
- 链表的初始状态:
head -> A -> B - 线程 1 执行,准备把 节点 A POP,让
head 指向 节点 B - 线程 1 保存
head 状态,然后准备执行 CAS (Node* oldHead = head.load()) - 此时线程 2 取得控制权,它执行了
POP -> POP -> PUSH A,链表最终状态: head -> A - 线程 1 执行,对比通过(因为
head 指向 A),执行了 POP,但预期的链表状态 head -> B 变成了 head -> 已经被释放的 Node B 的地址
那版本号是如何解决这个问题的呢?先来看代码:
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
| // 有版本号的 C++ 实现
typedef struct Head{
Node* nodes;
uint64_t ver; // 使用 uint64_t 保证内存对齐
} Head;
class LinkList {
std::atomic<Head> head;
Node* pop(){
Head oldHead = head.load();
Head newHead;
do{
if(oldHead.nodes == nullptr) return nullptr;
newHead.nodes = oldHead.nodes->next;
newHead.ver = oldHead.ver + 1;
} while(head.compare_exchange_weak(oldHead, newHead) == false);
return oldHead.nodes;
}
void push(int val){
Node* newNode = new Node{nullptr, val};
Head oldHead = head.load();
Head newHead;
do{
newHead.nodes = newNode;
newHead.ver = oldHead.ver + 1;
newNode->next = oldHead.nodes;
}while(head.compare_exchange_weak(oldHead, newHead) == false);
}
};
|
现在看再次按照上次的流程执行会发生什么:
- 链表的初始状态:
head -> A -> B - 线程 1 执行,准备把 节点 A POP,让
head 指向 节点 B - 线程 1 保存
head 状态,然后准备执行 CAS (Node* oldHead = head.load()) - 此时线程 2 取得控制权,它执行了
POP -> POP -> PUSH A,链表最终状态: head -> A - 线程 1 执行,对比没有通过,因为
head 的版本号不一样了,此时会更新数据并进行下一次 CAS 尝试,避免了野指针!