“拖了好久,万事开头难,终于开始动手做CMU15445了,开篇文章记录下来”
1.环境搭建:
1.1我用的是 WSL+vscode+cmake+clangd
wsl安装看微软官方文档(也可找自己喜欢的教程):
wsl中插件如下图:
具体环境搭建见这篇博客
CMU15445 2023spring环境准备 | JoyTsing
1.2下载《bustub-master》:
课程官网(Fall 2023):
CMU 15-445/645 :: Intro to Database Systems (Fall 2023)
github主页:
cmu-db/bustub: The BusTub Relational Database Management System (Educational) (github.com)
如果是按着主页教程一步步往下做的话,当操作到:
git push https://github.com/student/bustub-private.git master
此时不出意外的话会出意外,这也是我遇到的第一个拦路虎:
当你按提示输入github用户名和密码时,会发现报错。这是因为此时应该输入的Password并不是你的账户登录密码,而是动态令牌。
为解决这个问题,我们先进入github,右上角点头像,先点settings
最下面,Developer settings
Tokens:
然后按提示生成Token串,那一串东西也就是我们上面出错的地方应该输的密码!
不过这里不建议这样远程拉取,我们直接在主页下载压缩包,CV到/Ubuntu22.04/home/user 下,再在vscode里打开:
先查看更多版本:
找到Fall 2023
弄好之后打开vscode, 远程连接上wsl,此时应该是这样的:
然后按着主页一步步BUILD:
sudo build_support/packages.sh
这里不出意外的话会出意外,也是我遇到的第二个拦路虎:
这里出错涉及到packages.sh文件的读写权限,感谢我的舍友帮我解决,大家照着图中一步步输入:
ll
ll build_support
sudo chmod -R 777 ./
然后重新输入:
sudo build_support/packages.sh
就可以继续按着官网主页顺利完成BUILD环节了!
$ mkdir build
$ cd build
$ cmake ..
$ make
If you want to compile the system in debug mode, pass in the following flag to cmake: Debug mode:
$ cmake -DCMAKE_BUILD_TYPE=Debug ..
$ make -j`nproc`
环境搭建也到此结束。
2.TASK 1
俗话说得好:”万事配环境难!“现在我们已经搭建好了环境,接下来就摩拳擦掌,正式进行P0的编写
在 task1 中我们一共需要用到三个文件:
/src/include/primer/trie.h
/src/primer/trie.cpp
test/primer/trie_test.cpp
tire.h:头文件,需要我们读懂,并且用到一些变量名
tire.cpp:在此文件里写代码
tire_test.cpp:给定好的测试文件,用来测试TASK 1是否通过
在写代码之前一件很很很重要的事情:代码格式!!!
真心建议大家好好学习一下,并在编写代码的时候耐心遵守,一方面是最后提交的时候会少很多麻烦。另一方面,优秀的代码风格跟我们的发型,衣服一样——很帅!
2.1 trie.h文件学习笔记
在开始写之前,由于我的c++基础极其差,仅仅是完成学习《c++》课的课程要求,平时刷刷leetcode,所以很多东西都是第一次见,尤其是很多c++11之后的新标准,新写法。所以下面的笔记也相当于补课了。
explicit 关键字:防止隐式转换:默认情况下,C++ 允许通过单参数构造函数进行隐式类型转换。使用 explicit
可以阻止这种行为,只允许显式调用构造函数
智能指针
在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)C++11标准引入了几种智能指针类型,其中std::unique_ptr
和 std::shared_ptr
是最常用的。
std::unique_ptr
:独占所有权的智能指针,一个对象只能被一个unique_ptr
拥有。std::shared_ptr
:共享所有权的智能指针,多个shared_ptr
可以指向同一个对象,通过引用计数管理对象生命周期。
std::unique_ptr
特点
- 独占所有权:一个
unique_ptr
实例独占其所管理的对象,不能被拷贝,只能被移动。 - 轻量级:相较于
shared_ptr
,unique_ptr
更轻量,没有引用计数的开销。 - 自动释放:当
unique_ptr
被销毁时,它所管理的对象会自动被释放。 - 支持自定义删除器:可以自定义对象的删除方式。 使用方法
创建 std::unique_ptr
auto ptr2 = std::make_unique<int>(20);
复制 由于unique_ptr
不支持拷贝,只能通过移动来转移所有权。
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2 = std::move(ptr1); // ptr1 现在为空,ptr2 拥有对象
std::shared_ptr
特点
- 共享所有权:多个
shared_ptr
可以共享同一个对象,通过引用计数管理对象生命周期。 - 引用计数:每个
shared_ptr
实例维护一个引用计数,记录有多少个shared_ptr
指向同一个对象。 - 线程安全:引用计数的增加和减少是线程安全的,但对象本身的操作不一定是线程安全的。
- 支持自定义删除器:可以指定自定义删除器来管理对象的释放。
创建 std::shared_ptr
auto ptr2 = std::make_shared<int>(20);
复制 std::shared_ptr
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = ptr1; // 引用计数增加
最佳实用tips:
-
优先使用
std::unique_ptr
:当资源具有独占所有权时,优先选择unique_ptr
,因为它更高效且语义明确。std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
-
在需要共享所有权时使用
std::shared_ptr
:当多个对象需要共享同一资源时,使用shared_ptr
。std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); std::shared_ptr<MyClass> ptr2 = ptr1; // 共享所有权
-
使用
make_unique
和make_shared
:这两个工厂函数不仅语法更简洁,还能提高性能和安全性,避免new
操作带来的潜在异常问题。auto ptr1 = std::make_unique<MyClass>(); auto ptr2 = std::make_shared<MyClass>();
-
在容器中使用智能指针:当需要在容器中存储对象时,使用智能指针可以确保对象的生命周期与容器同步。
std::vector<std::shared_ptr<MyClass>> vec; vec.emplace_back(std::make_shared<MyClass>());
2.2 GET方法:
官方注释:
You should walk through the trie to find the node corresponding to the key. If the node doesn't exist, return nullptr. After you find the node, you should use `dynamic_cast` to cast it to `const TrieNodeWithValue<T> *`. If dynamic_cast returns `nullptr`, it means the type of the value is mismatched, and you should return nullptr.Otherwise, return the value.
GET很简单,我们按照官方注释,直接遍历查找即可。
需要注意的是,提示要求我们在找到目标节点之后,需要使用`dynamic_cast`,这里对我来说就要再补一下课了:
dynamic_cast:是 C++ 中用于在类层次结构中进行类型转换的一种运算符。它主要用于在运行时执行安全的类型转换
dynamic_cast
主要用于以下场景:
- 向下转型(Downcasting):将基类指针或引用转换为派生类的指针或引用。
- 类型检查:在多态类层次结构中确定对象的实际类型。
- 安全转换:确保转换在运行时是有效的,避免类型错误。
只有在以下条件下,dynamic_cast
才能正常工作:
- 多态类型:参与转换的类至少有一个虚函数,这使得类拥有 RTTI。
- 有效的类层次结构:目标类型和源类型必须在同一个继承体系中。
转换结果:
- 指针转换:
- 成功:返回指向目标类型的指针。
- 失败:返回
nullptr
。
- 引用转换:
- 成功:返回引用类型。
- 失败:抛出
std::bad_cast
异常。
2.3 PUT方法:
PUT方法就有些复杂了,对比fall 2022版本的P0,fall 2023要求每次put都要返回一个新的TRIE实例,而不是在原有树上修改,这种设计在多线程环境或需要历史版本数据的场景中非常有用,也为TAKS2做了铺垫。
对照示意图,一种比较好的写法是递归!先设置一个递归函数,递归函数思路大致如下:
-
查找匹配的首字符:
-
遍历当前节点的所有子节点,寻找与键的第一个字符匹配的子节点。
-
如果找到匹配的子节点:
-
递归情况:如果键长度大于1(非叶子节点),克隆该子节点并递归插入剩余的键部分。
-
叶子节点:如果键长度为1(是叶子节点),创建一个
TrieNodeWithValue
节点,存储值T
。
-
-
-
未找到匹配的子节点:
-
如果当前节点没有匹配的子节点,根据键的长度决定如何插入:
-
单字符键:直接插入一个
TrieNodeWithValue
节点。 -
多字符键:创建一个新的空
TrieNode
,并递归插入剩余部分。
-
-
-
不可变性保证:
-
通过
Clone
方法克隆节点,确保原有节点不被修改。 -
更新子节点时,使用新的克隆节点或新创建的节点,确保Trie的不可变性。
-
有了递归函数,PUT就很好实现了:
-
处理空键:
-
如果
key
为空,将值存储在根节点。 -
创建一个新的
TrieNodeWithValue
作为根节点,存储值T
。 -
如果原根节点有子节点,新的根节点将保留这些子节点,并添加值
T
。
-
-
处理非空键:
-
克隆当前根节点(如果存在),以确保不可变性。
-
调用递归函数递归插入键值对。
-
返回一个新的
Trie
实例,包含更新后的根节点。
-
2.4 REMOVE方法:
put顺利实现后,remove就依葫芦画瓢了,思路基本一致:
先写一个递归函数,实现思路如下:
-
查找匹配的首字符:
-
遍历当前节点的所有子节点,寻找与键的第一个字符匹配的子节点。
-
如果未找到匹配的子节点,函数返回
false
,表示删除失败。
-
-
删除逻辑:
-
键长度为1:
-
检查对应的子节点是否为值节点(
is_value_node_
)。 -
如果不是值节点,说明键不存在,返回
false
。 -
如果是值节点:
-
且没有子节点:直接删除该子节点。
-
有子节点:将该子节点转换为普通的
TrieNode
,即删除其值,但保留子节点。
-
-
返回
true
,表示删除成功。
-
-
键长度大于1:
-
克隆当前子节点(确保不可变性)。
-
递归调用递归函数,传入克隆后的子节点和键的剩余部分(去掉首字符)。
-
如果递归调用返回
false
,表示删除失败,返回false
。 -
删除后处理:
-
如果克隆后的子节点没有子节点且不是值节点,删除该子节点。
-
否则,更新子节点为克隆后的子节点。
-
-
返回
true
,表示删除成功。
-
-
再实现Remove函数:
-
处理根节点为空的情况:
-
如果
root_
为空,说明Trie为空,直接返回当前Trie实例(无变化)。
-
-
处理空键(空字符串)的情况:
-
如果
key
为空,意味着要删除存储在根节点的值。 -
根节点是值节点:
-
且没有子节点:删除根节点,返回一个空的Trie。
-
有子节点:将根节点转换为普通的
TrieNode
,删除其值,但保留子节点,返回新的Trie。
-
-
根节点不是值节点:键不存在,返回当前Trie实例(无变化)。
-
-
处理非空键:
-
克隆当前根节点,确保不可变性。
-
调用
RemoveCycle
递归删除键值对。 -
删除成功:
-
如果新的根节点没有子节点且不是值节点,设置
new_root
为nullptr
(Trie为空)。 -
否则,使用更新后的
new_root
创建并返回一个新的Trie实例。
-
-
删除失败:键不存在,返回当前Trie实例(无变化)。
-
2.5 测试TASK1
写完REMOVE后,就可以编译运行trie_test.cpp 来测试是否通过了:
先进入build文件夹,编译一下:
然后运行一下:
看到一串绿色就是通过了!
3.TSAK 2
Task #2 - Concurrent Key-Value Store,要求我们为多线程环境实现并发键值存储
这里可见我的c++基础多差了,又狠狠补课,截图两张当时学习的图片:
补课理解“多线程并发控制之后”,继续前进吧!
TASK 2一共需要用到:
trie_store.h:头文件,依旧需要我们读懂,重点是其中的几类锁
trie_store.cpp:写代码,这里三个同名函数的具体实现都用的是TASK1中的。TASK2中我们只需要上一下锁,实现多线程方面的内容即可。
trie_store_test.cpp:测试TASK2
3.1 GET方法:
依旧很简单,照着注释写就行,只需要使用互斥锁保护一下根节点的读取即可。
Pseudo-code:
(1) Take the root lock, get the root, and release the root lock. Don't lookup the value in the
trie while holding the root lock.
(2) Lookup the value in the trie.
(3) If the value is found, return a ValueGuard object that holds a reference to the value and the
root. Otherwise, return std::nullopt.
3.2 PUT方法:
这里注释提示得很明显了,结合对trie.h的理解,不难想出,要先上一把读写锁。
除此之外,// The logic should be somehow similar to `TrieStore::Get`.也提示我们:要复制一份当前根节点,继续保护一下根节点,以保证每个线程都操作的是副本,不会直接修改母本。
3.3 Remove方法:
如法炮制即可,不做过多赘述。
3.4 测试TASK 2:
编译运行trie_store_test.cpp
这样TASK2就算完成了!
4.TASK 3:
task3是一个简单的调试测试,我们前面配好环境的话,这里应该很顺利,一步步按照注释提示来即可。
只是需要简单改一下路径,先还是编译一下trie_debug_test.cpp文件,在终端:
cd build
make -j12 trie_debug_test
然后直接点调试,弹出错误后,自动打开 launch.json 文件,然后把要进行调试的文件的路径加进去即可:
按照这个路径查看会发现上面编译后该路径下有了trie_debug_test.cmake 文件
然后打开trie_debug_test.cpp,在注释提示处加上断点,按F5或者直接点击侧栏启动调试:
最后一步步完成题目要求的三个问题,把答案填到trie_answer.h里即可:
5.TASK 4:
task4让我们试着调用一下SQL函数,体验一下,只需一步步按着题目要求来即可:
值得一提的是,正常写完两个函数,测试时,一步步运行到绿色框里的语句时,会莫名报错,且只有这一条会报错,我还死磕了很久,真是蠢啊现在看,红色框里会提示该报错是正常的,我们不必理会,直接忽略,不会影响最后的提交和分数的。
task4只需要我们简单编写两个函数,分别是:
/src/planner/plan_func_call.cpp 里的 :auto Planner::GetFuncCallFromFactory ()
/src/include/execution/expressions/string_expression.h 里的 auto Compute ()
整体不难,大家理解注释,基本上就是写几个条件语句进行调用。
到这里,我们就已经完成所有的代码工作了!!!
6.打包提交:
6.1 注册gradescope:
网址:
注册在youtube的课程上有提到,Course Entry Code:KK5DVJ
6.2 格式检查:
安装clang-fromat,clang-tidy
sudo apt-get install clang-format
apt-get install clang-tidy
然后在build文件夹下依次执行以下命令
make format
make check-lint
make check-clang-tidy-p0
6.3 打包
在build文件夹下调出终端输入
make submit-p0 (这一步出错可能是没安装zip打包软件,自己apt install一下就可以)
cd …
python3 gradescope_sign.py
然后按提示输入一系列信息,之后就会发现,多了一个GRADESCOPE.md文件
这也是23年新加的要求,没这个最后的提交是无法通过的。
7.已知意外情况(我踩的坑):
我早已经发现,我是天生大霉b,做什么都会比别人多遇到一些报错和意外情况......
7.1:make submit -p0 路径异常:
正常打包成zip文件后,打开会发现里面是整整齐齐的几个文件夹,里面包含了我们之前用过的所有.h 和.cpp文件。但如果检查(这里建议大家提交前都检查一下)发现里面只有几个不认识的文件,那就说明submit脚本路径有问题。
解决办法如下:
打开build文件夹内的makefile文件,找到submit-p0对应的文件路径:
第308行
按照上述路径,打开对应文件:/build/CMakeFiles/submit-p0.dir/build.make
找到第69 70行:
把错误路径改成:
cd /home/jack/bustub-20231227-2023fall && zip project0-submission.zip src/include/primer/trie_answer.h src/include/primer/trie_store.h src/include/primer/trie.h src/primer/trie_store.cpp src/primer/trie.cpp src/planner/plan_func_call.cpp src/include/execution/expressions/string_expression.h
其中 /home/jack/bustub-20231227-2023fall 记得改成自己的用户名
之后删掉原来的压缩包,重新打包即可。
7.2:格式错误:
代码格式有问题的话,即使本地测试全部正确,在线提交也还是直接一个大大的 “0”
如果本地进行格式检查并改了以后,不排除在线提交的时候还会报错,可能是因为本地检查的不彻底,这里就按在线提示一个个改即可。这里回旋镖了我开头说的一定要注意代码风格和习惯!!!
8.提交通过 && 个人感想:
历尽九九八十一难,最后提交压缩包后,终于:
当时看到这个 全绿 以后 真是眼泪都出来了。
开始做的时候c++基础真的很差,除了一点点要学语法知识,思考数据结构以外,我还是天生大霉b,总是会比别人做的时候多踩很多坑,从本文提到的第一个错误开始到最后的格式检查,耗费了好多好多耐心,中途拖拖拉拉差点丢着没做下去,想着放弃。
但总是会想到,无数次告诉自己:
1918年,北大图书馆里有一个湖南乡巴佬在开头写下:“由于我的职位低下,人们都不愿同我来往。”
但在结尾他却说:“在坚冰还盖着北海的时候,我看到了怒放的梅花。”
或许你会说这只是个P0,预热而已,但对我,付出的耐心和意志让我体验万分,万事开头难,正如那个湖南土农民说得一样,“青年人在做什么之前要准备好走曲折的路。”