Latch的读写模式
- 读模式:多个线程在同一时间能够读取相同的对象;如果一个线程获持有一个处于读模式的latch,另一个线程也能够获取该处于读模式的latch。
- 写模式:只有一个线程能够获取该对象;如果有其他线程持有处于任意模式下的latch,则其他线程是不能够获得write latch的。
- 相容性矩阵:
Latch的实现
Reader-Writer Locks
- 允许多个并发读者
- 必须管理读写队列来避免饿死
- 可以在自旋锁的基础上加以实现
可以用互斥量和条件变量来实现rwlatch
, 在src/include/common/rwlatch.h
中实现了 ReaderWriterLatch
类。
数据成员和构造析构函数
互斥量 mutex_
来保护当前的读者个数reader_count_
(读者队列上最多有 UINT_MAX
个读者)和是否有写者进入writer_entered_
。当可以获取wlatch或rlatch时,用条件变量 writer_
或reader_
唤醒其他读写线程。需要注意的是析构函数在析构 mutex_
变量的时候,先使用了 guard
进行保护;其次是成员变量的声明顺序。
using mutex_t = std::mutex;
using cond_t = std::condition_variable;
static const uint32_t MAX_READERS = UINT_MAX;
public:
ReaderWriterLatch() = default;
~ReaderWriterLatch() { std::lock_guard<mutex_t> guard(mutex_); }
private:
mutex_t mutex_;
cond_t writer_;
cond_t reader_;
uint32_t reader_count_{0};
bool writer_entered_{false};
获取读锁和释放读锁
获取读锁的时候,在第 6
行代码可以看到,当有写者进入或读者数量达到最大值时,新进入的读者会睡眠在条件变量 reader_
上;释放读锁的时候在 18~21
行首先检查是否有写者进入,如果释放后当前读者数量为0,则通知其中一个写线程,然后在 22~25
行检查当前读者数量,很明显当有读者持有该读锁时,其他读者也可以轻易获取该读锁,当释放该读锁后当前读者数量等于 MAX_READERS - 1
时,才可能会有其他读线程等待,此时需要唤醒它们。
01 /**
02 * Acquire a read latch.
03 */
04 void RLock() {
05 std::unique_lock<mutex_t> latch(mutex_);
06 while (writer_entered_ || reader_count_ == MAX_READERS) {
07 reader_.wait(latch);
08 }
09 reader_count_++;
10 }
11
12 /**
13 * Release a read latch.
14 */
15 void RUnlock() {
16 std::lock_guard<mutex_t> guard(mutex_);
17 reader_count_--;
18 if (writer_entered_) {
19 if (reader_count_ == 0) {
20 writer_.notify_one();
21 }
22 } else {
23 if (reader_count_ == MAX_READERS - 1) {
24 reader_.notify_one();
25 }
26 }
27 }
获取写锁和释放写锁
在第 6~8
行可以看到获取写锁时,如果当前已经有写者进入了,则其他读线程会睡眠在 reader_
上,不能获得进行读,即使当前没有写线程获取该锁,在 9~12
行可以看到,当读者数量不为0时,进入的写者也不能进行写操作,会睡眠在 writer_
条件变量上。释放写锁时需要注意第 21
行会通知所有的读写线程。
01 /**
02 * Acquire a write latch.
03 */
04 void WLock() {
05 std::unique_lock<mutex_t> latch(mutex_);
06 while (writer_entered_) {
07 reader_.wait(latch);
08 }
09 writer_entered_ = true;
10 while (reader_count_ > 0) {
11 writer_.wait(latch);
12 }
13 }
14
15 /**
16 * Release a write latch.
17 */
18 void WUnlock() {
19 std::lock_guard<mutex_t> guard(mutex_);
20 writer_entered_ = false;
21 reader_.notify_all();
22 }
页面上锁
在缓冲池管理器中 FetchPage()
的返回值是一个指向 Page
实例的指针(src/include/storage/ Page/Page.h
)。可以获取 Page
上的 latch
,但不能获取B+Tree
节点上的 latch
(内部节点和叶节点都不行) 。src/include/storage/page/page.h
增加了在页面上获取和释放latch的辅助函数。
/** Acquire the page write latch. */
inline void WLatch() { rwlatch_.WLock(); }
/** Release the page write latch. */
inline void WUnlatch() { rwlatch_.WUnlock(); }
/** Acquire the page read latch. */
inline void RLatch() { rwlatch_.RLock(); }
/** Release the page read latch. */
inline void RUnlatch() { rwlatch_.RUnlock(); }
/** Page latch. */
ReaderWriterLatch rwlatch_;
Blocking OS Mutex
Test-and-Set Spinlock
蟹行协议(crabbing protocol)
我们需要更新原始的单线程 B+Tree
索引,以便它能够支持并发操作。我们将使用课堂上和课本上描述的蟹行协议进行封锁。遍历索引的线程将获取然后释放 B+Tree
页面上的 latch
。一个线程只能在它的子页面被认为是“安全的”时释放父页面上的 latch
。注意,“安全”的定义可能会根据线程正在执行的操作的类型而变化。
- Search: 从根页面开始,获取子页面上的read ® latch,然后在到达子页面时释放父页面上的latch
- Insert: :从根页面开始,获取子页面上的write (W) latch。一旦child被锁定,检查它是否安全,在这种情况下,如果是不满的。如果child是安全的,释放所有祖先的锁。
- Delete: 从根页面开始,获取子页面上的write (W) latch。一旦child被锁定,检查它是否安全,在这种情况下,至少是半满。(注意:对于根页面,我们需要用不同的标准进行检查)如果child是安全的,释放所有祖先的锁。
Search(GetValue)的并发:
先看下单线程版本的GetValue
操作:
- 查找叶子页面
- 在叶子页面里面查找key
- unpin
- 返回结果
INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::GetValue(const KeyType &key, std::vector<ValueType> *result, Transaction *transaction) {
Page* leaf_page = FindLeafPage(key, false);
LeafPage *leaf = reinterpret_cast<LeafPage *>(leaf_page->GetData());
ValueType value;
bool ret = leaf->Lookup(key, &value, comparator_);
if (ret) {
result->emplace_back(std::move(value));
}
buffer_pool_manager_->UnpinPage(leaf->GetPageId(), false);
return ret;
}
着重需要考虑的是叶子页面的查找
INDEX_TEMPLATE_ARGUMENTS
Page *BPLUSTREE_TYPE::FindLeafPage(const KeyType &key, bool leftMost) {
if (IsEmpty()) {
return nullptr;
}
Page *page = buffer_pool_manager_->FetchPage(root_page_id_);
BPlusTreePage *root = reinterpret_cast<BPlusTreePage *>(page->GetData());
while (!root->IsLeafPage()) {
InternalPage *inner = reinterpret_cast<InternalPage *>(root);
auto page_id = leftMost ? inner->ValueAt(0) : inner->Lookup(key, comparator_);
buffer_pool_manager_->UnpinPage(inner->GetPageId(), false);
page = buffer_pool_manager_->FetchPage(page_id);
root = reinterpret_cast<BPlusTreePage *>(page->GetData());
}
return page;
}
Insert 的并发
先看下单线程版本的Insert
操作
INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::Insert(const KeyType &key, const ValueType &value, Transaction *transaction) {
if (IsEmpty()) {
StartNewTree(key, value);
return true;
}
return InsertIntoLeaf(key, value, transaction);
}
INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::InsertIntoLeaf(const KeyType &key, const ValueType &value, Transaction *transaction) {
// find the right leaf page as insertion target
page_id_t leaf_page_id = FindLeafPage(key, transaction, Operation::SEARCH);
Page *leaf_page = buffer_pool_manager_->FetchPage(leaf_page_id);
leaf_page->WLatch();
UnlockUnpinAncestor(transaction, Operation::SEARCH);
transaction->AddIntoPageSet(leaf_page);
LeafPage *leaf = reinterpret_cast<LeafPage *>(leaf_page->GetData());
// then look through leaf page to see whether insert key exist or not. If exist, return immdiately
ValueType useless_value;
if (leaf->Lookup(key, &useless_value, comparator_)) {
UnlockUnpinAncestor(transaction, Operation::INSERT);
return false;
}
// safe page
if (IsSafePage(leaf_page, Operation::INSERT)) {
// otherwise insert entry.
leaf->Insert(key, value, comparator_);
UnlockUnpinAncestor(transaction, Operation::INSERT);
return true;
}
// If the leaf is not safe, release all previous latches,
// and restart the transaction using previous Insert/Delete protocol
UnlockUnpinAncestor(transaction, Operation::INSERT);
root_mutex_.WLock();
if (IsEmpty()) {
StartNewTree(key, value);
root_mutex_.WUnlock();
//UnlockUnpinAncestor(transaction, Operation::INSERT);
return true;
}
leaf_page_id = FindLeafPage(key, transaction, Operation::INSERT);
leaf_page = buffer_pool_manager_->FetchPage(leaf_page_id);
leaf_page->WLatch();
transaction->AddIntoPageSet(leaf_page);
leaf = reinterpret_cast<LeafPage *>(leaf_page->GetData());
//ValueType useless_value;
if (leaf->Lookup(key, &useless_value, comparator_)) {
// buffer_pool_manager_->UnpinPage(leaf->GetPageId(), false);
UnlockUnpinAncestor(transaction, Operation::INSERT);
return false;
}
leaf->Insert(key, value, comparator_);
// Remember to deal with split if necessary.
if (leaf->GetSize() == leaf->GetMaxSize()) {
KeyType mid_key = leaf->KeyAt(leaf->GetSize() / 2);
BPlusTreePage *old_node = reinterpret_cast<BPlusTreePage *>(leaf);
BPlusTreePage *new_node = Split(old_node);
InsertIntoParent(old_node, mid_key, new_node, transaction);
buffer_pool_manager_->UnpinPage(new_node->GetPageId(), true);
}
// buffer_pool_manager_->UnpinPage(leaf->GetPageId(), true);
UnlockUnpinAncestor(transaction, Operation::INSERT);
return true;
}
注意:
- 根节点上的Lock和UnLock的时机,防止死锁
- 性能调优