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新引入的移动构造函数与移动赋值运算符的重载, 可以参考那篇文章, 传送门 很容易实现, 文章介绍的十分清楚.

需要注意的是在升级BasicPageGuardReadPageGuardWritePageGuard的时候需要获取读锁和写锁, 这个很有用, 例如我们在新获取一个页面, 新建一个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的锁

当某一个线程使用完这个锁的时候, 我们需要在合适的位置释放锁, 或者说尽快的释放锁, 只有尽快的释放锁, 其他线程才可以访问这个页面, 继续操作, 提高并行的效率.
释放的方式通常有两个:

  1. 显示的调用PageGuard 的 Drop() 函数来释放这个锁, 在 Drop() 函数中我们增加了不会重复释放锁的判断.
  2. 在局部变量退出时隐式的调用析构函数, 调用析构函数中的 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++ 的语法知识, 总体收获很多.

posted @ 2024-11-07 21:14  虾野百鹤  阅读(6)  评论(0编辑  收藏  举报