Project 2
- [Project 2](#Project 2 "#project-2")
- [Checkpoint 1](#Checkpoint 1 "#checkpoint-1")
- [Flexible layout](#Flexible layout "#flexible-layout")
- [Page Layout](#Page Layout "#page-layout")
- [Binary Search](#Binary Search "#binary-search")
- Insert
- [Split Leaf](#Split Leaf "#split-leaf")
- [Split Internal](#Split Internal "#split-internal")
- [Checkpoint 2](#Checkpoint 2 "#checkpoint-2")
- Delete
- [Determine nearby node](#Determine nearby node "#determine-nearby-node")
- Coalesce
- Redistribution
- Iterator
- Concurrency
- [Crabbing lock](#Crabbing lock "#crabbing-lock")
- [Data race](#Data race "#data-race")
- Deadlock
- Delete
- [Checkpoint 1](#Checkpoint 1 "#checkpoint-1")
Checkpoint 1
Flexible layout
假设 B+ Tree 的 leaf node 与 internal node 的最大大小分别为:leaf_max_size 和 internal_max_size,那么:
leaf node的元素个数最少为: <math xmlns="http://www.w3.org/1998/Math/MathML"> ⌈ ( n − 1 ) / 2 ⌉ \lceil (n-1)/2 \rceil </math>⌈(n−1)/2⌉,最多为: <math xmlns="http://www.w3.org/1998/Math/MathML"> n − 1 n-1 </math>n−1internal node的元素个数最少为: <math xmlns="http://www.w3.org/1998/Math/MathML"> ⌈ n / 2 ⌉ \lceil n/2 \rceil </math>⌈n/2⌉,最多为: <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n
这里有一个问题是,
internal node的第一个key为空,那么其对应的元素个数是否应该也发生改变。实际上,这个问题关系到internal node的理解,后面有答案
在我们的 page 的定义中,无论是 internal page 还是 leaf page,都是以一个 pair<KeyTYpe, ValueType> 为类型的 flexible array 作为结尾,因此二者本质上对于每个元素所采取的存储方式都是一样的
换句话说,对于 leaf page,flexible array 中的每一个下标都对应存储一个 pair,而 internal page 当中的 flexible array 中的每一个下标也对应存储一个 pair,唯一不同的是 internal page 的第一个 pair 的 first 不存储元素
因此,无论是 internal page size 还是 leaf page size,记录的都是当前该 page 已经存储的 pair 的数量,不会出现在实际存储的 ValueType 一样的情况下,internal size 比 leaf size 要少一个的情况,二者的下标均是从 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 到 <math xmlns="http://www.w3.org/1998/Math/MathML"> i n t e r n a l / l e a f s i z e − 1 internal/leaf size - 1 </math>internal/leafsize−1
对于二分查找而言,我们查找的对象是 KeyType,而 KeyType 是存储在 pair 的 first 部分里面,因此对于 internal page,我们需要查找的范围为: <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 , n ) [1,n) </math>[1,n);而对于 leaf page,我们需要查找的范围为: <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 0 , n ) [0,n) </math>[0,n)
对于二者的 flexible array 的详细结构,可以参考下图:

Page Layout
BPlusTreePage 为 BPlusInternalPage 和 BPlusLeafPage 的基类,它只提供最基本的三个元素,一共占 12 bytes
BPlusInternalPage:metadata占12 bytes;在这基础上增加了一个flexible array,后续的所有内容均由其填充BPlusLeafPage在这基础上增加了一个指向下一个leaf page id的元素,占4 bytes,因此metadata占16 bytes;剩余的所有内容由flexible array进行填充
需要说明的是,B+Tree 的一个节点占一个 page,这里的意思是:在 4KB 的内容中,B+Tree 的页面 metadata 分别占 12/16 bytes,其余的才是每一个 pair 存储的内容 。而每个 page 自身的 metadata 并不会存放在这 4KB 当中,这部分的数据是单独存放的
值得一提的是,对于 internal page,实际上每个 pair 的定义是:pair<KeyType, page_id_t>,而 对于 leaf page,每个 pair 的定义是:pair<KeyType, ValueType>。我们可以使用在 b_plus_tree.h 开头的 InternalPage 和 LeafPage 来对变量进行定义
在实际的调试过程中,RID 一般是占 <math xmlns="http://www.w3.org/1998/Math/MathML"> 8 8 </math>8 字节而 page_id_t 则是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 4 </math>4 字节,如果发现 internal page 的 flexible array 出现了 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 4 </math>4 字节的偏移,那么大概率就是页面的定义出现了错误
因此到此为止,我们也就理解了,
internal page当中存放的是 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 个元素(也就是 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 个pair), <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 个叶节点的page_id和 <math xmlns="http://www.w3.org/1998/Math/MathML"> n − 1 n-1 </math>n−1 个KeyType
Binary Search
由于我们需要频繁使用 std::lower_bound 和 std::upper_bound,因此在这里我们需要对二者的行为详细说明一下:
我们假定待查找区间内的元素为 element,我们传入的元素为 value
std::lower_bound 接受一个范围 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ s t a r t , e n d ) [start, end) </math>[start,end) 作为参数,它会将这个范围划分为两部分,前半部分满足 <math xmlns="http://www.w3.org/1998/Math/MathML"> e l e m e n t < v a l u e element<value </math>element<value,而后半部分自然是 <math xmlns="http://www.w3.org/1998/Math/MathML"> e l e m e n t > = v a l u e element>=value </math>element>=value。该函数会返回第一个使得 element < value 为 false 的值,也就是后半区间的起点
std::upper_bound 接受一个范围 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ s t a r t , e n d ) [start, end) </math>[start,end) 作为参数,它同样会将这个范围划分为两部分,前半部分满足 <math xmlns="http://www.w3.org/1998/Math/MathML"> e l e m e n t < = v a l u e element<=value </math>element<=value,后半部分自然是 <math xmlns="http://www.w3.org/1998/Math/MathML"> e l e m e n t > v a l u e element>value </math>element>value。它会返回第一个使得 value < element 为 true 的值,也就是后半区间的起点
举个例子,假设待查找区间为: <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ] [1,2,3,4,5,6,7,8,9,10] </math>[1,2,3,4,5,6,7,8,9,10],假设 value = 5,那么:
对于 lower_bound 而言,区间被划分为: <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 , 2 , 3 , 4 ] [1,2,3,4] </math>[1,2,3,4] 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 5 , 6 , 7 , 8 , 9 , 10 ] [5,6,7,8,9,10] </math>[5,6,7,8,9,10],后半区间的起点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 5 5 </math>5
对于 upper_bound 而言,区间被划分为: <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 , 2 , 3 , 4 , 5 ] [1,2,3,4,5] </math>[1,2,3,4,5] 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 6 , 7 , 8 , 9 , 10 ] [6,7,8,9,10] </math>[6,7,8,9,10],后半区间的起点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 6 6 </math>6
std::lower_bound 和 std::upper_bound 都可以传入一个 predicate 来自定义比较方式,该 predicate 的要求为(两个函数都一样):只要第一个参数小于 第二个参数,那么返回 true
例如,我们可以这样写(该函数返回 -1 表示第一个参数小于第二个参数):
cpp
int idx = std::lower_bound(cur_page->array_, cur_page->array_ + cur_page->size_, std::make_pair(key, ValueType()),
[&](const MappingType &e1, const MappingType &e2) {
return this->comparator_(e1.first, e2.first) == -1;
}) -
cur_page->array_;
Insert
insert 函数的大致思路在书中有伪代码给出,在此我们只关注几个重要的点:
Split Leaf
由于 leaf page 最多只能存放 <math xmlns="http://www.w3.org/1998/Math/MathML"> n − 1 n-1 </math>n−1 个元素,因此当其大小已经 等于 <math xmlns="http://www.w3.org/1998/Math/MathML"> n − 1 n-1 </math>n−1 时,再次插入一个元素就需要进行分裂。而 internal page 最多能存放 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 个指针,因此当其大小已经 等于 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 时,再插入一个元素,就需要进行分裂
书中的伪代码给出的做法是,先将原先旧节点的所有内容复制到一块新开的内存空间当中,然后将待插入的 pair 插入进去,最后在分别复制回原先的节点和新的节点
实际上,这一步我们可以简化。我们先确定待插入的 pair 应该在旧节点的哪个下标 idx 之后进行插入(可以用 std::upper_bound 找到),然后再确定总的元素个数 size 的大小(这等于旧节点的元素个数 OldSize 再加一)
然后,我们可以将旧节点中,区间 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ s i z e / 2 , i d x ] [size/2,idx] </math>[size/2,idx] 复制到新节点,单独对新的 pair 进行复制 ,然后再将旧节点中区间 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i d x + 1 , O l d S i z e ) [idx + 1, OldSize) </math>[idx+1,OldSize) 的值复制到新节点
需要说明的是,这里的 size 有可能为奇数,那么我们在分裂后,旧节点和新节点谁多一个元素都是允许的,只要保证前后统一即可。在我们实现当中,我是允许旧节点相比于新节点多一个元素,因为我认为这在一定程度上可以减少未来再次分裂的可能性。如果是这样做的话,那么旧节点的复制区间改为 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ s i z e / 2 + 1 , i d x ] [size/2+1,idx] </math>[size/2+1,idx] 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ i d x + 1 , O l d S i z e ] [idx+1,OldSize] </math>[idx+1,OldSize] 即可
具体可参考下图:

Split Internal
internal page 的分裂与 leaf page 的分裂在本质上都是一样的,只不过 internal page 的分裂多了一个不断向父节点插入新的 pair 的过程
由于 internal page 的第一个 key 为空,因此我们按照 leaf page 的分裂方式得到两个节点后,新节点的第一个 key 需要置空
由于 internal page 有一个向上传递的过程,传递的值为:新节点的第一个 key 以及新节点的 page_id ,也就是说我们需要将第一个 key 复制一遍后,将其置空,然后将 pair 向上传递,这样便完成了 internal page 的分裂操作
需要说明的是,在课本当中的伪代码里面,函数的定义为:insert_in_parent(node N, value K', node N')。这里的意思是,向节点 N 的父节点中插入一个 pair,其内容为 (K', N')
关于如何快速求出当前节点的父节点,由于对父节点插入的前提一定是对叶节点插入,因此我们可以在找叶节点的时候先将这条路径上的每个节点的
page_id记录在vector里面,然后每次从back()取值,并pop一遍,这样子可以保证每次的back()都是当前节点的父节点
具体参考下图:

对于B+树的内部节点,如果是找 second 的话,那么不能用二分查找
Checkpoint 2
Delete
B+Tree 的 delete 操作在当前节点不满足 <math xmlns="http://www.w3.org/1998/Math/MathML"> ⌈ n / 2 ⌉ \lceil n/2\rceil </math>⌈n/2⌉ 时(也就是 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( n + 1 ) / 2 (n+1)/2 </math>(n+1)/2),会发生 coalesce 和 redistribution,这也是 B+Tree 中较为复杂的部分,我们详细讨论这一部分
Determine nearby node
无论是 coalesce 还是 redistribution,我们都需要确定当前节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 的相邻节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′,我们只需要在当前节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 的父节点中找到其相邻节点即可。换句话说, <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 具有相同的父节点
需要注意的是,这里我们只能逐个遍历 父节点当中的内容,而不能简单地使用二分查找。这是因为我们是在父节点中查找当前节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 的 page id,而在父节点中,key 是有序的,而 page id 是无序的,因此我们只能逐个遍历
另外,为了保证与官方提供的 bpt-printer 相一致,我们优先选择后一个节点 。换句话说,只有当前节点是最后一个节点的时候,我们才会选择前一个节点,否则都是选择后一个节点。书中的伪代码是优先选择前一个节点,这样做没有问题,但会与官方提供的 bpt-printer 所产生的结果不一致
在后面的描述中,我会描述我的实现方法。我是按照官方的
bpt-printer的行为进行设计的,虽然与书中的描述不同,但有可视化的结果进行参考
除了确定相邻节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 以外,我们还需要确定 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 之间的那个 key,具体如下图:

Coalesce
如果当前节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 和相邻节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 当中的元素个数小于等于 单个节点的最大大小,那么我们便执行 coalesce
- 对于
leaf page,要求小于等于leaf_max_size_ - 1 - 对于
internal page,要求小于等于internal_max_size_
对于 leaf page 而言,我们只需要将 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 中的所有元素加到 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 中即可(或者反过来),然后对应设置以下 next_page_id_ 即可
对于 internal page 而言,我们需要将 key 以及 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 中的所有元素全部加到 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 中(或者将 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 中的所有元素加到 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 中)。对于内部节点,由于第一个 key 为空,因此我们需要将父节点的 key 加入到新合并得到的节点当中,随后再在父节点中删除 key 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N(或者 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′)
在父节点中,我们是以 key 为单位进行删除的,而 key 又与其对于的 page id 组成了一个整体。为了方便,无论当前节点与相邻节点的位置关系如何,我们始终删除后一个节点,具体如下图:

对于 leaf page 而言,只是少了将父节点的 key 加入到新合并得到的节点中这一操作,移动的方向以及删除的节点二者是一样的
Redistribution
如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的元素个数大于 单个节点的最大大小,那么我们需要执行 redistribution。该操作的本质是向相邻节点借一个元素过来
对于 leaf page 而言,我们需要考虑二者的位置关系:
- 如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 在 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的前面,那么我们将 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的第一个元素加到 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 的后面,将 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 中的所有元素向前移动一位,并将原先父节点当中的
key设置为 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的第一个key - 如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 在 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的后面,那么我们将 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 中的所有元素向后移动一位,将 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的最后一个元素加到 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 中,并将原先父节点当中的
key设置为 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 的第一个key

对于 internal page 而言,我们同样需要考虑二者的位置关系:
- 如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 在 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的前面,我们需要将父节点当中的
key以及 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 第一个元素当中的second加到 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 的后面 ,然后将父节点当中的key替换为 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 第一个元素当中的first,最后将 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的所有元素向前移动一位 - 如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 在 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的后面,我们需要将 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 先向后移动一位,然后将父节点的
key以及 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的最后一个元素的second加到 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 的前面 ,然后用 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ′ N' </math>N′ 的最后一个元素的first替换掉父节点的key

Iterator
对于 iterator 的设计,由于该迭代器是只读的,并且我们需要保证在并发的时候不会发生 data race,因此我们需要一个 ReadPageGuard;另外,我们还需要获取当前 page 的下一个 page,因此我们需要一个 bpm 指针来获取;最后,由于我们需要确定当前 leaf page 中的元素的位置,因此需要一个 const Mapping* 的指针
实际上有这三个就以及足够了,我的设计当中加入了一个指向当前 leaf page 尾部的指针,用于快速确定当前迭代器是否已经遍历到头了
我的定义如下:
cpp
ReadPageGuard read_guard_;
const MappingType *ptr_;
const MappingType *end_;
page_id_t cur_page_id_;
BufferPoolManager *bpm_;
Concurrency
Crabbing lock
并发控制是这个实验最难的一个部分了,我们需要去实现 crabbing lock,这里需要用到 Context 这个工具
crabbing lock 的本质是:对于当前节点而言,如果我们能够确定它的子节点 是 safe 的,那么我们便可以释放当前节点 的所有祖宗 ancestors(不包括当前节点 )。而一个节点是 safe 的当且仅当它不会执行 split, coalesce, redistribution 这些操作。换句话说,对于 insert 操作而言,子节点的元素个数加一小于 <math xmlns="http://www.w3.org/1998/Math/MathML"> n − 1 n-1 </math>n−1,对于 delete 操作而言,子节点的元素个数减一大于等于 <math xmlns="http://www.w3.org/1998/Math/MathML"> ⌈ n / 2 ⌉ \lceil n/2\rceil </math>⌈n/2⌉
因此,我们在向下递归到叶节点时便可以对当前节点的子节点进行判断,进而确定是否需要释放之前的节点
实际上,如果我们确定了当且节点的子节点是安全的,那么相当于无论子节点发生什么操作,都不会影响到当前节点的之上的节点 。我们不释放当前节点的原因在于,子节点的操作会传递到当前节点,也就是我们需要对当前节点进行修改,因此我们不能释放当前节点
由于我们是对 page 进行上锁,因此如果前面的 thread 持有该节点的 lock,那么后续的 thread 会卡在该节点。实际上,我们需要细化这个过程,不然会出现很多问题。后续的 thread 会先从 bpm 中获取到该 page,然后在试图对该 page 上锁时被卡住。也就是说后续节点是获得了当前的页面,只是无法对当前页面进行写入而已
我们考虑这样一种情况,有两个线程都执行插入操作, <math xmlns="http://www.w3.org/1998/Math/MathML"> A A </math>A 线程在插入时, <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 会被卡在根节点(此时它已经获得了根节点的 page 内容,只不过无法写入),如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> A A </math>A 的插入导致 B+Tree 的根节点发生了变化,那么此时根节点的位置便发生了改变,而 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 目前所被卡住的位置并不是新的根节点,因此这会产生问题。
顺带一提,这种情况只会在插入的时候产生错误,在删除时不会。因为哪怕根节点被删除, <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 线程已经持有了原先的根节点,因此它不会被
bpm从内存当中清空出去。并且 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 也可以顺利地从原先根节点的内容找到正确的路径
关于这个问题,我的解决办法是,先将原先根节点的 page id 保存下来,然后 fetch 一次,随后将原先的 page id 与根节点当前的的 page id 进行比较,如果不同则再次 fetch 一次,即:
cpp
page_id_t old_root_id = this->header_page_id_;
auto root_guard = this->bpm_->FetchPageWrite(old_root_id);
if (old_root_id != this->header_page_id_) {
root_guard = this->bpm_->FetchPageWrite(this->header_page_id_);
}
但这个代码会产生问题。我们注意看 root_guard = this->bpm_->FetchPageWrite(this->header_page_id_) 这行代码,会执行这行代码说明根节点发生了改变。当前的 root_guard 已经持有了旧的根节点的 lock,然后它试图获取新的根节点。在执行完 if 判断时,如果该线程在将要执行这条语句时被中断,如果其他的线程所执行的操作导致新的根节点被删去,也就是现在整棵树的根节点与最开始的根节点变成同一个(这种情况是可能的,因为新的根节点此时还没有被 lock),那么当当前线程执行时,它会对同一个 page id 执行两次 lock,这会导致错误(在执行 WriteGuard 的移动赋值语句时,会先执行 FetchPageWrite)
因此我将上述代码修改为:
cpp
page_id_t old_root_id = this->header_page_id_;
auto root_guard = this->bpm_->FetchPageWrite(old_root_id);
if (old_root_id != this->header_page_id_) {
root_guard.Drop();
root_guard = this->bpm_->FetchPageWrite(this->header_page_id_);
}
对于多线程当中的 data race 和 deadlock,在此写下一点我的经验
Data race
需要说明的是,
Race和Contention是两个不同的概念:
Contention是指线程 <math xmlns="http://www.w3.org/1998/Math/MathML"> A A </math>A 以及访问了线程 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 而 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 需要等待 <math xmlns="http://www.w3.org/1998/Math/MathML"> A A </math>A 将该资源释放Race是指 <math xmlns="http://www.w3.org/1998/Math/MathML"> A A </math>A 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 都想要访问该资源,最快的那个将抢先访问,而慢的那个则只能等待。因此这会导致Contention
对于这种情况,我们可以在全部线程完成插入、删除、查询等操作后,逐一检查 B+Tree 当中的数据是否是正确的。换句话说,我们直接对所有的 leaf page 进行一次遍历,便可以确定当前存在的元素是否符合预期
Deadlock
如果判断产生 deadlock,我们直接进入 cgdb ,跳转到 join 的前一条语句当中。这时所有的线程一定都创建完毕并且开始执行。由于多线程的执行具有随机性,因此我们实际上是无法单步执行某个线程的。因此,我们可以故意让 deadlock 发生,然后跳转到对应的线程,通过 backtrace 来查看栈帧,并通过 frame 指令来跳转到对应的栈帧中,然后再对数据进行检查,进而明确 deadlock 发生的原因