Task2 Disk Scheduler
1 task2介绍
主要介绍一下这个task目的是什么:Diskscheduler类的构造函数中启动了一个线程,用于接收BufferPoolManager发来的读写磁盘请求,并将其放入一个请求队列(request_queue_)中;然后启动一个新线程(background_thread_),不断从请求队列中获取请求,根据请求类型调用对应DiskManager的读写函数进行磁盘读写。
- Schedule(DiskRequest r):接收请求并放入请求队列
- StartWorkerThread():线程函数,从请求队列中获取新请求,并根据请求类型调用磁盘读写函数
读写的内容在Channel里(已经写好了,直接用就行),注意读写完成之后要给promise类型的那个记录变量赋值(callback_),在下一个task中就能发现promise的作用。
2前置内容介绍
2.1std::future和std::promise
主要用于异步编程和线程间通信。一个常见的使用方式是,在一个线程中创建一个 std::promise 对象,并从它获取一个 std::future 对象。然后,这个 std::future 对象可以被传递到另一个线程,该线程可以调用 get() 来等待结果。当原始线程完成计算并准备好结果时,它可以使用std::promise::set_value() 来设置结果。
过程:
1.首先,在一个线程中创建一个std::promise对象。这个对象将用于在将来某个时间点设置一个值或异常。
2. 通过调用std::promise对象的get_future()成员函数,获取一个与之关联的std::future对象。这个std::future对象将用于在另一个线程中获取异步操作的结果。
3. 在创建std::promise和std::future对象的线程中,执行异步操作。
4. 当异步操作完成时,通过调用std::promise对象的set_value()方法(用于设置结果值)或set_exception()方法(用于设置异常)来传递结果。这通常在执行异步操作的线程中完成。
5.在另一个线程中,使用之前获取的std::future对象的get()方法来获取异步操作的结果。如果异步操作尚未完成,get()方法会阻塞当前线程,直到结果可用。
std::promise和std::future是一次性的,即一个std::promise对象只能被设置一次值,而std::future对象只能获取一次结果。如果需要多次通信,可以考虑使用其他线程同步机制,如条件变量、互斥锁等。此外,使用std::future的wait_for或wait_until方法可以实现非阻塞的等待,并可以设置超时时间。
2.2 join与detach
假设background_thread是一个线程的名字。
- 当调用background_thread_->join();时,当前线程(调用join的线程)将阻塞,直到background_thread_所代表的线程完成执行。一旦background_thread_的线程完成,join函数将返回,并且当前线程可以继续执行后续的代码;
- 当调用background_thread_->detach();时,表示你不再关心它代表的线程的完成情况,也不希望等待它。这样,background_thread_对象可以被销毁或重新赋值,即使线程还在运行。
2.3lambda表达式
是一种匿名函数,即没有函数名的函数。它可以捕获一定范围内的变量并在一个函数体内对这些变量进行操作。可以方便地定义短小的函数,而无需正式地定义一个具有函数名的函数。同时,Lambda表达式可以作为参数传递给其他函数
例:[&] { StartWorkerThread();
这个lambda表达式通过引用捕获其所在作用域的所有变量。将调用StartWorkerThread()函数。
小知识:
析构函数:对象销毁时调用
emplace:emplace方法直接在std::thread对象内部构造线程,而不需要先创建一个线程对象然后再赋值给std::thread。它接受与构造函数相同的参数,并直接用于初始化线程对象。
std::optional和std::make_optional区别:std::make_optional 是一个函数模板,它用于创建并初始化一个 std::optional 对象。使用 std::make_optional 可以避免显式调用 std::optional 的构造函数,使得代码更加简洁易读。
3代码
DiskScheduler::DiskScheduler(DiskManager *disk_manager) : disk_manager_(disk_manager) {
// Spawn the background thread
background_thread_.emplace([&] { StartWorkerThread(); });
}
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();
}
}
void DiskScheduler::Schedule(DiskRequest r) {
request_queue_.Put(std::make_optional<DiskRequest>(std::move(r)));
}
void DiskScheduler::StartWorkerThread() {
std::optional<DiskRequest> request;
// 是否循环看request.has_value()
while ((request = request_queue_.Get(), request.has_value())) {
if (request->is_write_) {
disk_manager_->WritePage(request->page_id_, request->data_);
} else {
disk_manager_->ReadPage(request->page_id_, request->data_);
}
// 请求已处理完成,值设为true
request->callback_.set_value(true);
}
}
Task3 Buffer Pool Manager
1 task3 内容介绍
BufferpoolManager里面存放了空闲的frame号以及frameid与pageid的映射,在取page的时候:
(1)page在缓冲池里面,直接返回page。
(2)page不在缓冲池里面,且缓冲池满,驱逐一个旧page(task1的lur-k)然后调用diskscheduler(task2)将新page读入这个frame。这里需要用到future和promise(task2 的2.1已经介绍过了)。线程A创建一个promise,然后获得promise对应的future,然后线程A将promise交给线程B(此处是diskscheduler的后台读写磁盘线程),在线程B未给promise赋值的时候,线程A读取future的值会阻塞,当线程B完成读写之后给promise赋值,这时线程A才能继续运行。
(3)缓冲池不满,直接从磁盘中读取page。
关于page_id、frame_id、page_table的关系见下图。
2 代码详解
2.1头文件(buffer_pool_manager.h)
2.1.1各种变量名含义
变量名 | 含义 |
---|---|
pool_size_ | 缓冲池大小,即缓冲池中能够容纳的页面数量 |
next_page_id_ | 下一个页面ID,用于分配新的页面ID |
*pages_ | 页面数组,存储缓冲池中的所有页面 |
disk_scheduler_ | 磁盘调度器,用于调度磁盘上的读写操作 |
*log_manager_ | 日志管理器,用于管理日志 |
page_table_ | 将页面ID映射到帧ID,用于跟踪缓冲池中的页面位置 |
replacer_ | 替换器,用于把pin=0页面驱逐,以释放空间给新的页面 |
free_list_ | 空闲帧列表,存储当前没有页面驻留的帧ID |
latch_ | 加锁用的 |
2.1.2方法列表
方法名 | 作用 |
---|---|
FetchPage(page_id_t page_id) | 给定物理页id,获取该物理页所对应的frame |
UnpinPage(page_id_t page_id, bool is_dirty) | 将指定的物理页对应的frame的pin_count减1 |
FlushPage(page_id_t page_id) | 将给定的缓存页写回磁盘 |
NewPage(page_id_t* page_id) | 找到一个空闲的frame,新分配一个物理页,并将该物理页的内容读取到刚找到的这个frame中 |
DeletePage(page_id_t page_id) | 给定物理页id,将物理页对应的frame从buffer中删除 |
FlushAllPages() | 将所有有效的缓存页写回磁盘 |
2.2FlushPage
2.2.1思路:
思路和给的注释一样,注释里说忽略is_dirty位,不知道为什么,我感觉应该判断的。
2.2.2代码
auto BufferPoolManager::FlushPage(page_id_t page_id) -> bool {
// 如果page对象不包含物理页面
if (page_id == INVALID_PAGE_ID) {
return false;
}
std::scoped_lock lock(latch_);
// 如果映射里没有
if (page_table_.find(page_id) == page_table_.end()) {
return false;
}
// 获得page_id在缓冲池中的位置
auto page = pages_ + page_table_[page_id];
// 写回,这里creatpromise方法返回了一个std::promise对象
auto promise = disk_scheduler_->CreatePromise();
auto future = promise.get_future();
disk_scheduler_->Schedule({true, page->GetData(), page->GetPageId(), std::move(promise)});
future.get();
// 赃位恢复
page->is_dirty_ = false;
return true;
}
2.3 FlushAllPages
2.3.1思路
和flushpage基本一样,只是由从写回固定的页面到把缓冲池所有有效的都写回的区别。
2.3.2代码
void BufferPoolManager::FlushAllPages() {
std::scoped_lock lock(latch_);
for (size_t current_size = 0; current_size < pool_size_; current_size++) {
// 获得page_id在缓冲池中的位置
auto page = pages_ + current_size;
if (page->GetPageId() == INVALID_PAGE_ID) {
continue;
}
// 和flush方法一样
auto promise = disk_scheduler_->CreatePromise();
auto future = promise.get_future();
disk_scheduler_->Schedule({true, page->GetData(), page->GetPageId(), std::move(promise)});
future.get();
page->is_dirty_ = false;
}
}
2.4 UnpinPage
2.4.1思路
把注释里的直接粘过来了,写的很好
从缓冲区池中取消锁定目标页面。如果页面ID不在缓冲区池中或其锁定计数已经为0,则返回false。
减少页面的锁定计数。如果锁定计数达到0,则替换器应该能够驱逐该帧。
同时,设置页面上的脏标志,以指示页面是否被修改过。
@param page_id 要取消锁定的页面的ID
@param is_dirty 如果页面应被标记为脏,则为true;否则为false
@param access_type 对页面的访问类型,仅用于排行榜测试。
@return 如果页面不在页面表中或其锁定计数在此调用之前已经小于等于0,则返回false;否则返回true。
2.4.2代码
auto BufferPoolManager::UnpinPage(page_id_t page_id, bool is_dirty, [[maybe_unused]] AccessType access_type) -> bool {
if (page_id == INVALID_PAGE_ID) {
return false;
}
std::scoped_lock lock(latch_);
if (page_table_.find(page_id) == page_table_.end()) {
return false;
}
auto frame_id = page_table_[page_id];
auto page = pages_ + frame_id;
// 设置脏位,如果原本是脏的或传进的is_dirty是脏的,最终就是脏的
page->is_dirty_ = is_dirty||page->is_dirty_;
// if pin count is 0
if (page->GetPinCount() == 0) {
return false;
}
// pin要-1
page->pin_count_ -= 1;
// 如果-1后为0,调用lru-k中的SetEvictable方法,把帧设为可驱逐的
if (page->GetPinCount() == 0) {
replacer_->SetEvictable(frame_id, true);
}
return true;
}
2.5NewPage
2.5.1思路
写累了,直接粘了
在缓冲池中创建一个新页面。将 page_id 设置为新页面的ID,或者,如果所有的帧(frames)当前都在使用并且不可驱逐(换句话说,就是都已“固定”(pinned)),则设置为 nullptr。
你应该从空闲列表(free list)或替换器(replacer)中选择一个替换帧(总是先从空闲列表中查找),然后调用 AllocatePage() 方法来获取一个新的页面ID。如果替换帧中有一个脏页面(dirty page),你应该先将其写回到磁盘中。你还需要为新页面重置内存和元数据。
记住通过调用 replacer.SetEvictable(frame_id, false) 来“固定”(Pin)这个帧,这样在缓冲池管理器“解除固定”(Unpins)之前,替换器不会驱逐这个帧。此外,为了 LRU-K 算法能够正常工作,记得在替换器中记录这个帧的访问历史。
输出参数 page_id:创建页面的ID
返回值:如果不能创建新页面,则返回 nullptr;否则,返回指向新页面的指针
2.5.2代码
// 在缓冲池中创建一个新页面
auto BufferPoolManager::NewPage(page_id_t *page_id) -> Page * {
Page *page;
frame_id_t frame_id = -1;
std::scoped_lock lock(latch_);
// 如果free_list_里有值
if (!free_list_.empty()) {
// 获取free_list_容器的最后一个元素并移除,并让page为新内存地址
frame_id = free_list_.back();
free_list_.pop_back();
page = pages_ + frame_id;
} else {
// free_list_里没值,看replacer_里有没有能替换的
if (!replacer_->Evict(&frame_id)) {
return nullptr;
}
page = pages_ + frame_id;
}
// 和flushpage方法一样,如果page地址上原frame里存放的从内存中拿出的page_id对应的页是脏的
if (page->IsDirty()) {
auto promise = disk_scheduler_->CreatePromise();
auto future = promise.get_future();
disk_scheduler_->Schedule({true, page->GetData(), page->GetPageId(), std::move(promise)});
future.get();
// ! clean
page->is_dirty_ = false;
}
// 获取一个新的页面ID(注释里给的)
*page_id = AllocatePage();
// 把旧的映射删掉
page_table_.erase(page->GetPageId());
// 建立新的映射
page_table_.emplace(*page_id, frame_id);
// 把新page的参数更新下
page->page_id_ = *page_id;
page->pin_count_ = 1;
// ResetMemory方法:将页面中的所有数据清零
page->ResetMemory();
// 更新replacer_
replacer_->RecordAccess(frame_id);
replacer_->SetEvictable(frame_id, false);
return page;
}
2.6FetchPage
2.6.1思路
给定物理页id,获取该物理页所对应的frame。
有两种情况
(1)先在缓冲池里找这个页面,如果有直接返回就行(别玩pin数+1)
(2)如果没有,和上面的newpage一样,申请一个新帧。区别在于,newpage放进去的内容是AllocatePage()给的,这个是从磁盘读取的。
2.6.2代码
auto BufferPoolManager::FetchPage(page_id_t page_id, [[maybe_unused]] AccessType access_type) -> Page * {
if (page_id == INVALID_PAGE_ID) {
return nullptr;
}
std::scoped_lock lock(latch_);
if (page_table_.find(page_id) != page_table_.end()) {
// ! get page
auto frame_id = page_table_[page_id];
auto page = pages_ + frame_id;
// 更新replacer
replacer_->RecordAccess(frame_id);
replacer_->SetEvictable(frame_id, false);
// ! update pin count
page->pin_count_ += 1;
return page;
}
// Newpage 方法里的
Page *page;
frame_id_t frame_id = -1;
if (!free_list_.empty()) {
frame_id = free_list_.back();
free_list_.pop_back();
page = pages_ + frame_id;
} else {
if (!replacer_->Evict(&frame_id)) {
return nullptr;
}
page = pages_ + frame_id;
}
if (page->IsDirty()) {
auto promise = disk_scheduler_->CreatePromise();
auto future = promise.get_future();
disk_scheduler_->Schedule({true, page->GetData(), page->GetPageId(), std::move(promise)});
future.get();
page->is_dirty_ = false;
}
page_table_.erase(page->GetPageId());
page_table_.emplace(page_id, frame_id);
page->page_id_ = page_id;
page->pin_count_ = 1;
page->ResetMemory();
replacer_->RecordAccess(frame_id);
replacer_->SetEvictable(frame_id, false);
// 从磁盘中读页,读完后写回(之前的是写完后写回)
auto promise = disk_scheduler_->CreatePromise();
auto future = promise.get_future();
disk_scheduler_->Schedule({false, page->GetData(), page->GetPageId(), std::move(promise)});
future.get();
return page;
}
2.7DeletePage
2.7.1思路
依旧是注释的粘贴
从缓冲区池中删除一个页面。如果页面ID不在缓冲区池中,则不做任何操作并返回true。如果
页面被固定且无法删除,则立即返回false。
在从页面表中删除页面后,停止在替换器中跟踪该帧,并将帧添加回空闲列表。同时,重置页面的内存和元数据。最后,调用DeallocatePage()来模仿在磁盘上释放页面。
2.7.2代码
auto BufferPoolManager::DeletePage(page_id_t page_id) -> bool {
if (page_id == INVALID_PAGE_ID) {
return true;
}
std::scoped_lock lock(latch_);
// 如果页面存在
if (page_table_.find(page_id) != page_table_.end()) {
auto frame_id = page_table_[page_id];
auto page = pages_ + frame_id;
// 如果页面用着呢
if (page->GetPinCount() > 0) {
return false;
}
// 删除页面
page_table_.erase(page_id);
free_list_.push_back(frame_id);
replacer_->Remove(frame_id);
// 把内存该清的清,page的参数该换的换
page->ResetMemory();
page->page_id_ = INVALID_PAGE_ID;
page->is_dirty_ = false;
page->pin_count_ = 0;
}
// 注释里要求的:调用DeallocatePage()来模仿在磁盘上释放页面。
DeallocatePage(page_id);
return true;
}
线程内容真什么都不会,一直在摸鱼,感觉好难不想写。后面就是查查查,结果理解了以后发现一点都不难,,比lru-k简单多了!我为什么能拖这么长时间,我真服了我自己了。
一口气干了5个点,啊!!饿死我了!!
补个图,满分!
参考文章
[1]https://zhuanlan.zhihu.com/p/674080359(CMU15445 Fall2023 Project 0-4 通关全记录)
[2]https://blog.csdn.net/cpp_juruo/article/details/134215386(【CMU 15-445】Proj1 Buffer Pool Manager)
[3]https://blog.csdn.net/Altair_alpha/article/details/127745308?spm=1001.2014.3001.5506