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
发生的原因