CMU15445 2024Spring 课程作业

关于运行环境与提交

运行环境可以阅读我的博客:

https://editor.csdn.net/md/?articleId=137051445

提交打分需要,首先去https://www.gradescope.com/注册一个账号,但是可悲的,我好像查不到 Spring 2024的课程邀请码,我翻遍了课程首页和ppt,也没能找到一个邀请码。如果有同学找到了或者有测试群,请务必拉我一下,感谢!

Does CMU 15-445/645 Spring 2024 has a Gradescope submission site
available to non-CMU students?

PROJECT #1 - BUFFER POOL

作业的详细描述原文如下:

https://15445.courses.cs.cmu.edu/spring2024/project1

整体上说,需要完成四个子任务:

  • LRU-K Replacement Policy
  • Disk Scheduler
  • Buffer Pool Manager
  • Read/Write Page Guards

四个子任务的实现顺序最好按照描述顺序依次进行,否则需要花费很多时间去阅读理解相关依赖代码。在项目代码中,每个子任务都提供了对应的自测用例,在完成作业后可以使用测试用例或者自己额外增加测试用例来完成验证。

Task #1 - LRU-K Replacement Policy

第一个任务是完成LRU-K缓存替换算法。

算法原理

简单地说就是,访问次数不足K次的块,替换最近访问时间最早的(退化为LRU算法)。访问次数大于等于K次的块,根据倒数第K次的访问时间与当前时间的距离作为其距离(替换权重),权重越大的越容易被替换。
比如K=2,5个块的访问时间历史如下,时间从1开始,当前时间为12:

块1:1
块2:2
块3:3、6、9、11
块4:4、7、
块5:5、8、10

由于访问次数不足K,块1与块2的LRU-K距离为正无穷(+inf);块 3的距离为12-9=3;块4的距离为12-4=8;块5的距离为:12-8=4。
如果五个块均可以被替换,那么根据LRU-K算法,块1将最先被替换出去,接着被替换出去的是块2,然后依次是4、5、3。

相关代码解读

bustub框架给定了缓存替换管理的基本单位:frame。
如下图,替换策略服务的对象buffer_pool中储存了page_id与frame_id的Map,
buffer_pool
通过page_id与frame_id的1对1Map进行映射,buffer_pool保存了page所在的buffer槽。通过抽象出替换策略的若干个接口(如:pin-钉住frame不允许被换出、victim-清空一个frame并返回其frame_id)和frame_id,buffer_pool实现了替换策略功能的下放,实现了替换策略的可拓展性。不同替换策略的实际frame结构,需要保存不同的数据,比如LRU-K需要保存历史访问记录,而LRU只需要保存最新的访问时间即可。
简而言之,替换策略实际上管理了若干虚拟的内存槽位(也就是存放page的内存区),这些槽位通过frame_id表示。替换策略无需管理实际的page,只需要记录frame_id的访问、读取、权限等信息,并提供替换的策略即可。
下面是替换策略的头文件,也是全部待实现的函数:

 public:
  /**
   *
   * TODO(P1): Add implementation
   *
   * @brief a new LRUKReplacer.
   * @param num_frames the maximum number of frames the LRUReplacer will be required to store
   */
  explicit LRUKReplacer(size_t num_frames, size_t k);

  DISALLOW_COPY_AND_MOVE(LRUKReplacer);

  /**
   * TODO(P1): Add implementation
   *
   * @brief Destroys the LRUReplacer.
   */
  ~LRUKReplacer() = default;


  /**
   * TODO(P1): Add implementation
   *
   * @brief Find the frame with largest backward k-distance and evict that frame. Only frames
   * that are marked as 'evictable' are candidates for eviction.
   *
   * A frame with less than k historical references is given +inf as its backward k-distance.
   * If multiple frames have inf backward k-distance, then evict frame with earliest timestamp
   * based on LRU.
   *
   * Successful eviction of a frame should decrement the size of replacer and remove the frame's
   * access history.
   *
   * @param[out] frame_id id of frame that is evicted.
   * @return true if a frame is evicted successfully, false if no frames can be evicted.
   */
  auto Evict(frame_id_t *frame_id) -> bool;

  /**
   * TODO(P1): Add implementation
   *
   * @brief Record the event that the given frame id is accessed at current timestamp.
   * Create a new entry for access history if frame id has not been seen before.
   *
   * If frame id is invalid (ie. larger than replacer_size_), throw an exception. You can
   * also use BUSTUB_ASSERT to abort the process if frame id is invalid.
   *
   * @param frame_id id of frame that received a new access.
   * @param access_type type of access that was received. This parameter is only needed for
   * leaderboard tests.
   */
  void RecordAccess(frame_id_t frame_id, AccessType access_type = AccessType::Unknown);

  /**
   * TODO(P1): Add implementation
   *
   * @brief Toggle whether a frame is evictable or non-evictable. This function also
   * controls replacer's size. Note that size is equal to number of evictable entries.
   *
   * If a frame was previously evictable and is to be set to non-evictable, then size should
   * decrement. If a frame was previously non-evictable and is to be set to evictable,
   * then size should increment.
   *
   * If frame id is invalid, throw an exception or abort the process.
   *
   * For other scenarios, this function should terminate without modifying anything.
   *
   * @param frame_id id of frame whose 'evictable' status will be modified
   * @param set_evictable whether the given frame is evictable or not
   */
  void SetEvictable(frame_id_t frame_id, bool set_evictable);

  /**
   * TODO(P1): Add implementation
   *
   * @brief Remove an evictable frame from replacer, along with its access history.
   * This function should also decrement replacer's size if removal is successful.
   *
   * Note that this is different from evicting a frame, which always remove the frame
   * with largest backward k-distance. This function removes specified frame id,
   * no matter what its backward k-distance is.
   *
   * If Remove is called on a non-evictable frame, throw an exception or abort the
   * process.
   *
   * If specified frame is not found, directly return from this function.
   *
   * @param frame_id id of frame to be removed
   */
  void Remove(frame_id_t frame_id);

  /**
   * TODO(P1): Add implementation
   *
   * @brief Return replacer's size, which tracks the number of evictable frames.
   *
   * @return size_t
   */
  auto Size() -> size_t;

 private:
  // TODO(student): implement me! You can replace these member variables as you like.
  // Remove maybe_unused if you start using them.


  std::list<frame_id_t> hot_frames;  // 记录了热点缓存块,当一个frame的访问次数大于K时,将被加入此队列
  std::list<frame_id_t> cold_frames;  // 访问次数未达K的Frame
  std::unordered_map<frame_id_t, LRUKNode> node_store_;
  size_t current_timestamp_{0};
  size_t curr_size_{0};
  size_t replacer_size_;
  size_t k_;
  std::mutex latch_;

可以看到,除了构造、析构函数之外,需要实现Evict、RecordAccess、SetEvictable、Remove、Size这几个函数。Evict实现了通过替换策略清空一个frame,在buffer_pool中将使用这个方法为新加载page做准备。RecordAccess实现了记录访问,在buffer_pool中每次访问页都将调用这个方法去记录访问历史。Size函数记录了当前替换策略中可以被替换出的frame数量,如果没有可以被替换的frame,且全部frame都已经被使用,那么buffer_pool就无法执行加载新page的方法。

完成思路

基本思路是双队列。cold队列存储那些访问次数不达K的frame_id,而hot队列存储访问次数达K的frame_id。如果某个frame在访问后次数达到了K,那么将其移动到hot队列中。
双队列
具体的实现不在此过多赘述。

测试

原测试用例
这里需要将第二个形参的前缀去除,修改后如下图:
启用测试用例
然后cd进入build文件夹,make后执行单测。

make lru_k_replacer_test j8
./test/lru_k_replacer_test

Task #2 - Disk Scheduler

相关代码解读

bustub已经实现了一个多线程共享队列Channel,可以作为磁盘请求调度器的工具类,这个让任务调度的实现可以专注于调度算法。

The disk scheduler will utilize a shared queue to schedule and process the DiskRequests. One thread will add a request to the queue, and the disk scheduler’s background worker will process the queued requests. We have provided a Channel class in src/include/common/channel.h to facilitate the safe sharing of data between threads, but feel free to use your own implementation if you find it necessary.

下面是Channel类的源码:
Channel
Channel类是一个模板类,提供了一个对队列进行PUT和GET的方法,使用两个信号量保障了多线程的效率。
Put方法和Get方法在首行都通过unique_lock获取独占信号量m_,用于控制每次只允许一个线程对队列进行访问/修改。不同的是,Put方法在完成对队列的插入后提前释放了信号量,并没有使用到unique_lock的自动释放。接着Put方法唤醒所有等待condition_variable的线程,也就是唤醒所有的消费者队列线程。而Get方法在调用 cv_.wait() 时,需要传入一个已经锁定了的互斥量(也就是m_),cv_.wait() 在等待期间会自动释放 lk 所锁定的互斥量,并且在收到通知后重新锁定。这样可以确保在等待期间不会造成死锁,并且在条件满足(被notify_all,且队列不为空)时重新获取互斥量的锁定,接着进行后续的出队操作。
在disk_scheduler中,bustub还提供了一个用于保存工作线程的Optional

  /** The background thread responsible for issuing scheduled requests to the disk manager. */
  std::optional<std::thread> background_thread_;

然后是析构函数

DiskScheduler::~DiskScheduler() {
  // Put a `std::nullopt` in the queue to signal to exit the loop
  request_queue_.Put(std::nullopt);
  if (background_thread_.has_value()) {
    background_thread_->join();
  }
}

在析构函数中,首先通过 request_queue_.Put(std::nullopt); 向请求队列 request_queue_ 中放入了一个 std::nullopt,用于向后台线程发出退出信号。这是一种常见的通过特殊值来通知线程退出的方法。然后,通过 background_thread_.has_value() 判断后台线程是否已经启动,如果已经启动,则调用 background_thread_->join(); 来等待后台线程结束。join() 方法会阻塞当前执行析构函数的线程,直到后台工作线程全部执行完成再继续完成析构。
此外,disk_scheduer还调用了DiskManager 类提供的读、写等基本函数从磁盘中获取真实数据或者写入数据。具体DiskManager源码解读可见另一篇博客:
TODO: 插入链接

完成思路

这个作业的任务是实现两个函数:

  • Schedule(DiskRequest r)
  • StartWorkerThread()

Schedule的作用是将一个磁盘读写请求添加到任务队列中,而StartWorkerThread的任务是启动一个不主动停止的磁盘读写任务处理线程,这个工作线程将被Scheduler保存,并在调用析构函数时传递一个std::nullopt的任务来停止。
在此基础上,任务并没有规定我们具体的任务调度算法是什么,操作系统中常用的任务调度算法有若干,下面列出一些常见的任务调度算法:

  • 先来先服务 (FCFS):按照任务提交的顺序进行调度,先提交的任务先执行,后提交的任务排队等待执行。FCFS 算法简单易实现,但可能导致长任务等待时间过长,造成资源利用不足和响应时间延迟。
  • 最短作业优先 (SJF):选择预计执行时间最短的任务优先执行。SJF 算法能够最大程度地减少平均等待时间,但可能会造成长任务长期等待短任务完成而无法执行。
  • 最短剩余时间优先 (SRTF):在 SJF 算法的基础上,动态调整任务的执行顺序,优先执行剩余执行时间最短的任务。SRTF 算法可以更加灵活地响应短任务的到来,但需要频繁地进行任务切换,增加了系统开销。
  • 轮转调度 (Round Robin):将任务按照提交顺序排成一个队列,每次选择队头任务执行一定时间片后,将其放到队尾等待下一次调度。轮转调度算法简单高效,能够保证每个任务都有机会执行,但可能导致长任务的响应时间较长。
  • 多级反馈队列调度 (MFQ):将任务分成多个队列,每个队列有不同的优先级,高优先级队列的任务先执行,如果任务未完成,则降低其优先级,将其移到低优先级队列继续执行。MFQ 算法能够兼顾短任务和长任务,提高了系统的整体性能。
  • 最高响应比优先 (HRRN):选择响应比最高的任务优先执行,响应比定义为等待时间加上执行时间与执行时间的比值。HRRN 算法能够兼顾短任务和长任务,避免了 SJF 算法可能出现的长任务等待问题。
  • 最高优先权优先 (HPF):为每个任务分配一个优先级,优先级高的任务先执行。HPF 算法适用于实时系统和任务有明确优先级的场景,但可能导致低优先级任务长期等待高优先级任务完成。
  • 公平分享调度 (Fair Share Scheduling):根据任务的资源需求和历史使用情况进行资源分配,保证每个任务都能够获得公平的资源份额。公平分享调度算法适用于多用户、多任务共享资源的场景,能够确保系统资源的公平分配。

对于这个作业,可首先使用简单的先来先服务的调度算法完成任务,按照任务队列依次执行。即:只要在StartWorkerThread函数中不断Get请求,处理完成后设置请求的 std::promise callback_的future即可。
当然这里存在优化空间,等完成全部作业再继续优化。

测试

测试类很简单地提供了两个读写测试:
测试
值得一提的是,实际从外存读取数据的下层工具类diskmanager,这里并没有使用。而是使用的其专门用于测试的子类DiskManagerUnlimitedMemory,这个子类并没有执行从外存读取数据的功能,它的一切数据Page都是在内存上进行操作的,通过一定的延迟算法,模拟从外存上读写文件的延迟。具体实现可以阅读我源码解读的文章:
TODO:插入链接

make disk_scheduler_test -j16
./test/disk_scheduler_test

执行上面两条编译测试语句,便可以得到测试结果:
Disk_Scheduler测试结果

Task #3 - Buffer Pool Manager

相关代码解读

这个Task需要了解之前的两个Task完成的工具类的使用方法,如果忘记了可以去对应的test中查看。此外还需要使用到一些Page类的内部属性和方法。这里简单地列举并描述一下:
disk_scheduler1、disk_scheduler。BPM子任务的全部读写,都需要通过promise+future的方法异步执行,通过向disk_scheduler提交读写任务。参数为:是否写、数据位置、page_id(用于唯一标识一个块)、一个用于返回future的promise。
NewPage中如果遇到了frame中保存的是脏页,那么这里就需要发送一个写操作请求,将脏页数据写回才能继续执行。
lru_k_replacer_test
2、lru_k_replacer_test。完成BPM的前提是替换策略已实现,替换策略将为BPM提供四个主要的功能函数:RecordAccess、SetEvictable、Evict、Size。RecordAccess记录某个frame的使用,SetEvictable负责转换frame的可替换状态,Evict用于根据替换策略找到一个可替换frame,Size用于快速检查是否存在可替换frame。
3、page。此处源码解读可阅读我的博客:
TODO:插入链接

完成思路

这个任务需要完成src/buffer/buffer_pool_manager.cpp中的若干函数:

  • FetchPage(page_id_t page_id)
  • UnpinPage(page_id_t page_id, bool is_dirty)
  • FlushPage(page_id_t page_id)
  • NewPage(page_id_t* page_id)
  • DeletePage(page_id_t page_id)
  • FlushAllPages()
    NewPage、FetchPage、需要使用到Task #1 - LRU-K Replacement Policy中的RecordAccess函数,用于记录对块的读取记录。NewPage、FetchPage是最复杂的函数,这两个函数需要同时检查是否待替换的frame中保存的是脏页(如果是脏页还要记得写回)、调用替换策略提供的RecordAccess接口记录对frame的访问。
    具体函数的相互调用可以完全按照注解中描述一步一步编写,可以保障不遗漏。此外,一定要记得加锁,因为最终的测试是在多线程环境下测试的。
    简而言之,BPM对Page状态的管理是结合frame的evictable、page的pin_count_、is_dirty_综合设计策略的。在尝试获取新页(包括从外存加载、创建新页)时,较为复杂。BPM还为脏页的写回单独提供了两个函数FlushPage、FlushAllPages,这两个函数将重新将page对象的is_dirty_置为false。

测试

这里bustub提供了两个测试方法。
BinaryDataTest用于检查是否可以恢复包含终止符的页面。
SampleTest用于测试普通页面。
测试结果

Task #4 - Read/Write Page Guards

相关代码解读

PageGuard

测试
光阅读作业要求文档,可能难以理解PageGuard到底要干什么,所以结合bustub提供的测试代码可以更清晰地理解PageGuard的功能。
阅读测试代码。27-31行创建了bpm和模拟外存读取的disk_manager。33-34行通过bpm创建了一个新页。而36行将这个页通过BasicPageGuard包装。38-40行分别测试了page中的数据和BasicPageGuard包装接口得到的数据是否一致,并特殊检测了page的pincount是否为1,这个1是NewPage操作后自动增加的:
NewPage
接着42行调用了Drop函数,这个函数代表本PageGuard对page的使用结束了,因此会将pin_count减1。也就是44行的检查结果。
阅读了上面的测试代码,不难理解,PageGuard是对page实际使用的包装。对每个Page的每次使用,都将通过PageGuard进行包装控制。PageGuard会通过Drop结束它的生命周期,最终将page的pin_count_减1。在此基础上,PageGuard有三种类型(Base、Read、Write),Base可以转换为Read/Write,但是Read、Write不可互相转换也不可以逆向转为Base。下面是GPT对这三类设计的理解:
这些页面保护的设计可能是为了提供不同级别的页面访问权限和并发控制。

  • BasicPageGuard:这是最基本的页面保护类。它可能用于简单的页面访问情况,不涉及并发控制或权限升级。
  • ReadPageGuard:这是用于读取页面的保护类。它可能包含读取页面时需要的并发控制逻辑,以确保多个线程可以同时读取页面而不会发生冲突。
  • WritePageGuard:这是用于写入页面的保护类。它可能包含写入页面时需要的并发控制逻辑,以确保写入操作的原子性和一致性。
BPM

除了上面的PageGuard外,还要读一下BPM的待实现函数。BPM相对简单,只有三个函数,也就是提供对page访问的包装函数,在里面创建PageGuard。
BPM
和注释中描述的一样,除了返回的是一个被页面保护程序包装好的page之外,几乎和FetchPage没什么区别。

完成思路

首先完成BasePageGuard中的各个函数。按照注解中描述的逻辑,除了两个update函数会对Page加锁保证page不会被换出,其它全部的函数都默认PageGuard已经获取了锁。
我找到了一个不太清楚是哪个版本的测试代码:

//===----------------------------------------------------------------------===//
//
//                         BusTub
//
// page_guard_test.cpp
//
// Identification: test/storage/page_guard_test.cpp
//
// Copyright (c) 2015-2019, Carnegie Mellon University Database Group
//
//===----------------------------------------------------------------------===//

#include <cstdio>
#include <random>
#include <string>

#include "buffer/buffer_pool_manager.h"
#include "storage/disk/disk_manager_memory.h"
#include "storage/page/page_guard.h"

#include "gtest/gtest.h"

namespace bustub {

// NOLINTNEXTLINE
TEST(PageGuardTest, SampleTest) {
  const size_t buffer_pool_size = 5;
  const size_t k = 2;

  auto disk_manager = std::make_shared<DiskManagerUnlimitedMemory>();
  auto bpm = std::make_shared<BufferPoolManager>(buffer_pool_size, disk_manager.get(), k);

  page_id_t page_id_temp;
  auto *page0 = bpm->NewPage(&page_id_temp);

  auto guarded_page = BasicPageGuard(bpm.get(), page0);

  EXPECT_EQ(page0->GetData(), guarded_page.GetData());
  EXPECT_EQ(page0->GetPageId(), guarded_page.PageId());
  EXPECT_EQ(1, page0->GetPinCount());

  guarded_page.Drop();

  EXPECT_EQ(0, page0->GetPinCount());

  {
    auto *page2 = bpm->NewPage(&page_id_temp);
    std::cout << "page2" << page2->GetPageId() << "" << std::endl;
    auto guard2 = ReadPageGuard(bpm.get(), page2);
    std::cout << "guard2" << guard2.PageId() << "" << std::endl;
  }

  // Shutdown the disk manager and remove the temporary file we created.
  disk_manager->ShutDown();
}

TEST(PageGuardTest, MoveTest) {
  const std::string db_name = "test.db";
  const size_t buffer_pool_size = 10;
  const size_t k = 2;

  auto disk_manager = std::make_shared<DiskManagerUnlimitedMemory>();
  auto bpm = std::make_shared<BufferPoolManager>(buffer_pool_size, disk_manager.get(), k);

  Page *init_page[6];
  page_id_t page_id_temp;
  for (int i = 0; i < 6; ++i) {
    init_page[i] = bpm->NewPage(&page_id_temp);
  }

  BasicPageGuard basic_guard0(bpm.get(), init_page[0]);
  BasicPageGuard basic_guard1(bpm.get(), init_page[1]);
  basic_guard0 = std::move(basic_guard1);
  BasicPageGuard basic_guard2(std::move(basic_guard0));

  BasicPageGuard basic_guard3(bpm.get(), init_page[2]);
  BasicPageGuard basic_guard4(bpm.get(), init_page[3]);
  ReadPageGuard read_guard0(bpm.get(), init_page[2]);
  ReadPageGuard read_guard1(bpm.get(), init_page[3]);
  read_guard0 = std::move(read_guard1);
  ReadPageGuard read_guard2(std::move(read_guard0));

  init_page[4]->WLatch();
  init_page[5]->WLatch();
  WritePageGuard write_guard0(bpm.get(), init_page[4]);
  WritePageGuard write_guard1(bpm.get(), init_page[5]);

  // Important: here, latch the page id 4 by Page*, outside the watch of WPageGuard, but the WPageGuard still needs to
  // unlatch page 4 in operator= where the page id 4 is on the left side of the operator =.
  // Error log:
  // terminate called after throwing an instance of 'std::system_error'
  //   what():  Resource deadlock avoided
  // 1/1 Test #64: PageGuardTest.MoveTest
  write_guard0 = std::move(write_guard1);
  init_page[4]->WLatch();  // A deadlock will appear here if the latch is still on after operator= is called.

  WritePageGuard write_guard2(std::move(write_guard0));

  // Shutdown the disk manager and remove the temporary file we created.
  disk_manager->ShutDown();
}

}  // namespace bustub

在倒数第6行,可以看到

// A deadlock will appear here if the latch is still on after operator= is called.

我们反向推理,发生死锁的前提是init_page[4]已经被锁住了,也就是 write_guard0 = std::move(write_guard1);没有释放锁,但是接着向上读,可以发现除了

init_page[4]->WLatch();

之外,并没有任何加锁的操作。而在这个锁之后,调用了构造函数,可以确定构造函数并不会加锁,接着调用了移动构造函数,而这里会发生死锁,所以可以确定移动构造函数将释放原本page的锁。
根据上面的规律,我们可以推断出:除了两个特殊描述的update函数会对Page加锁保证page不会被换出,其它全部的函数都默认PageGuard已经获取了锁(BasePageGuard并没有锁,在Update函数中加锁),只需要在Drop和移动构造函数处需要释放锁。
推理它们的功能实在是太难了,因为几乎没有任何地方调用了这些PageGuard让我能理解它们的作用和锁管理的机制。

测试

在原本测试代码的基础上,我又添加了一些测试。下面是全部的测试代码:

//===----------------------------------------------------------------------===//
//
//                         BusTub
//
// page_guard_test.cpp
//
// Identification: test/storage/page_guard_test.cpp
//
// Copyright (c) 2015-2019, Carnegie Mellon University Database Group
//
//===----------------------------------------------------------------------===//

#include <cstdio>
#include <random>
#include <string>

#include "buffer/buffer_pool_manager.h"
#include "storage/disk/disk_manager_memory.h"
#include "storage/page/page_guard.h"

#include "gtest/gtest.h"

namespace bustub {

// NOLINTNEXTLINE
TEST(PageGuardTest, SampleTest) {
  const size_t buffer_pool_size = 5;
  const size_t k = 2;

  auto disk_manager = std::make_shared<DiskManagerUnlimitedMemory>();
  auto bpm = std::make_shared<BufferPoolManager>(buffer_pool_size, disk_manager.get(), k);

  page_id_t page_id_temp;
  auto *page0 = bpm->NewPage(&page_id_temp);

  auto guarded_page = BasicPageGuard(bpm.get(), page0);

  EXPECT_EQ(page0->GetData(), guarded_page.GetData());
  EXPECT_EQ(page0->GetPageId(), guarded_page.PageId());
  EXPECT_EQ(1, page0->GetPinCount());

  guarded_page.Drop();

  EXPECT_EQ(0, page0->GetPinCount());

  {
    auto *page2 = bpm->NewPage(&page_id_temp);
    std::cout << "page2" << page2->GetPageId() << "" << std::endl;
    auto guard2 = ReadPageGuard(bpm.get(), page2);
    std::cout << "guard2" << guard2.PageId() << "" << std::endl;
  }

  // Shutdown the disk manager and remove the temporary file we created.
  disk_manager->ShutDown();
}

TEST(PageGuardTest, MoveTest) {
  const std::string db_name = "test.db";
  const size_t buffer_pool_size = 10;
  const size_t k = 2;

  auto disk_manager = std::make_shared<DiskManagerUnlimitedMemory>();
  auto bpm = std::make_shared<BufferPoolManager>(buffer_pool_size, disk_manager.get(), k);

  Page *init_page[6];
  page_id_t page_id_temp;
  for (int i = 0; i < 6; ++i) {
    init_page[i] = bpm->NewPage(&page_id_temp);
  }

  BasicPageGuard basic_guard0(bpm.get(), init_page[0]);
  BasicPageGuard basic_guard1(bpm.get(), init_page[1]);
  basic_guard0 = std::move(basic_guard1);
  BasicPageGuard basic_guard2(std::move(basic_guard0));

  BasicPageGuard basic_guard3(bpm.get(), init_page[2]);
  BasicPageGuard basic_guard4(bpm.get(), init_page[3]);
  ReadPageGuard read_guard0(bpm.get(), init_page[2]);
  ReadPageGuard read_guard1(bpm.get(), init_page[3]);
  read_guard0 = std::move(read_guard1);
  ReadPageGuard read_guard2(std::move(read_guard0));

  init_page[4]->WLatch();
  init_page[5]->WLatch();
  WritePageGuard write_guard0(bpm.get(), init_page[4]);
  WritePageGuard write_guard1(bpm.get(), init_page[5]);

  // Important: here, latch the page id 4 by Page*, outside the watch of WPageGuard, but the WPageGuard still needs to
  // unlatch page 4 in operator= where the page id 4 is on the left side of the operator =.
  // Error log:
  // terminate called after throwing an instance of 'std::system_error'
  //   what():  Resource deadlock avoided
  // 1/1 Test #64: PageGuardTest.MoveTest
  write_guard0 = std::move(write_guard1);
  init_page[4]->WLatch();  // A deadlock will appear here if the latch is still on after operator= is called.

  WritePageGuard write_guard2(std::move(write_guard0));

  // Shutdown the disk manager and remove the temporary file we created.
  disk_manager->ShutDown();
}

}  // namespace bustub

测试结果是通过了,但是我还是不满意。介于目前没办法提交Gradescope,所以只能先这么将就着了。

Leaderboard Task (Optional)

PROJECT #2 - EXTENDIBLE HASH INDEX(In learning)

这段时间乱七八糟的事情太多了没空学,现在出来实习了,看看能不能抽空做一下。目前准备先做22年秋的,

Task #1 - Extendible Hash Table Pages

Task #2 - Extendible Hashing Implementation

Task #3 - Concurrency Control

  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值