CMU_15445_P2_PageGuard
CMU_15445_P2_PageGuard
我将页面守护部分与多线程调用部分放在一起写在这篇博客中了, 页面守卫的本质是更加优雅方便的使用内存中的页(Pages). 我们知道Buffer_Pool_Manager实际上是管理页面, BPM 管理的是页面在内存中的组织形式与磁盘交互等, PageGuard
为其他进程包装了使用页面的方法, 其中主要封装了页面读写锁的获取与释放, 以及配合 BPM PIN或者 UNPIN 页面.
举例来说就是:
某个进程想使用页面的时候, 可以初始化一个 BPM 的实例, 将一个键值对写进页面, 首先调用 BPM 的
NewPage()
函数新建一个使用的页面, 然后获取页面的读写锁, 然后写完释放这个页面的读写锁, 这样本身没什么问题, 但是我们经常容易忘记 UNPIN 这个页面, 以及将读写锁与释放锁写进代码中容易被忽略, 因此使用 PageGaurd 来优雅的使用页面.
BasicPageGuard
Basic PageGuard 的定义如下:
private:
friend class ReadPageGuard;
friend class WritePageGuard;
BufferPoolManager *bpm_{nullptr}; // 管理内存的BPM
Page *page_{nullptr}; // 这个 PageGuard 守护的页面
bool is_dirty_{false}; // 当前页面是否被修改, 是否为 Dirty
我们需要实现的是重载 BasicPageGuard
的移动构造函数与移动赋值运算符, 在前面的文章中我介绍了C++11新引入的移动构造函数与移动赋值运算符的重载, 可以参考那篇文章, 传送门 很容易实现, 文章介绍的十分清楚.
需要注意的是在升级BasicPageGuard
为ReadPageGuard
和WritePageGuard
的时候需要获取读锁和写锁, 这个很有用, 例如我们在新获取一个页面, 新建一个PageGuard, 然后想立刻写这个PageGuard, 此时就需要将这个PageGuard升级, 方式如下:
// 将基础的Page guard 升级为读的Page guard
auto BasicPageGuard::UpgradeRead() -> ReadPageGuard {
if (this->page_ != nullptr) {
this->page_->RLatch();
}
auto upgraded_read_guard = ReadPageGuard{this->bpm_, this->page_};
this->bpm_ = nullptr;
this->page_ = nullptr;
return upgraded_read_guard;
}
// 将基础的 Page guard 升级为写的 Page guard
auto BasicPageGuard::UpgradeWrite() -> WritePageGuard {
if (this->page_ != nullptr) {
this->page_->WLatch();
}
auto upgraded_write_guard = WritePageGuard{this->bpm_, this->page_};
this->bpm_ = nullptr;
this->page_ = nullptr;
return upgraded_write_guard;
}
另一点需要注意的是PageGuard的Drop()
函数. Drop()
函数是释放这个 PageGuard, 需要将这个页面守卫使用的资源释放, 最重要的是, 需要在这里 UNPIN 这个页面.
void BasicPageGuard::Drop() {
// Drop 的时候, 表示不再继续使用该页面, 所以会 UnpinPage
if (this->bpm_ != nullptr && this->page_ != nullptr) {
this->bpm_->UnpinPage(this->page_->GetPageId(), is_dirty_);
}
this->bpm_ = nullptr;
this->page_ = nullptr;
}
ReadPageGuard 和 WritePageGuard
这两个的实现在实现 BasicPageGuard之后, 参考相关的实现很容易完成, 需要注意的就是 Drop()
函数, 因为ReadPageGuard 与 WritePageGuard 需要在这里释放锁, 并且需要避免重复的释放锁, 我的实现如下:
void ReadPageGuard::Drop() {
if (this->guard_.page_ != nullptr) {
this->guard_.page_->RUnlatch();
}
this->guard_.Drop();
}
void WritePageGuard::Drop() {
if (this->guard_.page_ != nullptr) {
this->guard_.page_->WUnlatch();
}
this->guard_.Drop();
}
另一个有坑的点是, 这里的移动复制运算符中, 我们需要先 Drop()
当前指针, 虽然 ReadPageGuard 与 WritePageGuard 中只有一个属性 gurad, 但是我们需要 Drop 的不是this->guard
, 而是 this
, 因为实际上这个过程还涉及到锁的释放.
PageGuard 中读写锁的使用
由于在Project2中引入了多线程, 多线程是指同时有多个线程操作我们可扩展Hash表, 前面在实现可扩展Hash表的时候知道, 可扩展Hash表中的Header, Directory, Bucket 都是按照页面大小申请的, 因此我们线程切换也是按照一个 Page 大小切换, 使用PageGuard管理.
使用PageGuard获取页面的锁
我们可以升级BPM中获取页面的方式, 在获取页面的同时获取这个页面(Page)的读写锁, 这一部分比较简单, 我的实现如下:
auto BufferPoolManager::FetchPageRead(page_id_t page_id) -> ReadPageGuard {
Page *new_page = nullptr;
new_page = this->FetchPage(page_id);
// 获取读锁
new_page->RLatch();
return {this, new_page};
}
auto BufferPoolManager::FetchPageWrite(page_id_t page_id) -> WritePageGuard {
Page *new_page = nullptr;
new_page = this->FetchPage(page_id);
// 获取写锁
new_page->WLatch();
return {this, new_page};
}
// 新申请一个页面的时候无法判断是读这个页面还是写这个页面, 可以调用上面的 UpgradeRead 或者 UpgradeWrite 获取对应的锁即可
auto BufferPoolManager::NewPageGuarded(page_id_t *page_id) -> BasicPageGuard {
Page *new_page = nullptr;
new_page = this->NewPage(page_id);
return {this, new_page};
}
每个访问可扩展Hash表的线程都使用上面的方式获取这个页面的读写锁, 获取读写锁之后才可以继续对这个页面进行操作, 同时天梯榜的优化也是从这个角度考虑的.
在合适的位置释放PageGuard的锁
当某一个线程使用完这个锁的时候, 我们需要在合适的位置释放锁, 或者说尽快的释放锁, 只有尽快的释放锁, 其他线程才可以访问这个页面, 继续操作, 提高并行的效率.
释放的方式通常有两个:
- 显示的调用PageGuard 的
Drop()
函数来释放这个锁, 在Drop()
函数中我们增加了不会重复释放锁的判断. - 在局部变量退出时隐式的调用析构函数, 调用析构函数中的
Drop()
函数释放锁.
PagrGuard 对Page数据的操作
PageGuard是一个页面守卫, 但是我们要做的通常是读取或者修改一个页面的内容, 这部分一开始我没有注意到, 后续实现的时候才注意到这其中的奥妙:
// 获取这个页面的数据, 是这个页面守卫 守卫的数据
auto GetData() -> const char * { return page_->GetData(); }
// 模板函数, 将申请到的页面转换为可扩展Hash的各种页面, 例如Header, 页目录, Bucket, 但是仅可读
template <class T>
auto As() -> const T * {
return reinterpret_cast<const T *>(GetData());
}
// 需要注意和 GetData() 的区别, GetData() 的返回值是一个 const 类型的指针, 是不可以修改的, 表示只允许读的数据
// GetDataMut 有两个不同, 它返回的是一个非 const 类型的数据, 表示可以修改这个页面的数据, 并直接将 is_dirty 设置为了 true
auto GetDataMut() -> char * {
is_dirty_ = true;
return page_->GetData();
}
// 模板函数, 将申请到的页面转换为可扩展Hash的各种页面, 例如Header, 页目录, Bucket, 表示可以写的页面
template <class T>
auto AsMut() -> T * {
return reinterpret_cast<T *>(GetDataMut());
}
上面对页面数据读取与修改的部分贯穿了可扩展Hash表的实现过程, 每次要访问可扩展Hash表的结构都需要先获取这个页面的数据, 使用示例如下:
// 获取Hash 表的表头页面结构体
auto header_guard = this->bpm_->FetchPageWrite(header_page_id_);
auto *header_page = header_guard.template AsMut<ExtendibleHTableHeaderPage>();
总结
这部分是我最不熟悉的, 和之前的 BPM 中多线程的设计都感觉对我来说十分的新奇, 并且设计的十分巧妙, 这次 Project2 的 PageGuard 的设计方式就很新奇, 能够很方便的保证线程安全, 和BPM共同作用, 这些都是之前没有接触的设计模型, 本次也学习了更多的 C++ 的语法知识, 总体收获很多.