当然,我不能使用这种投机取巧的方法去过测试,我又重看了前面的lock_shared方法,很快发现了问题,这里我换一批注释:
bool LockManager::LockShared(Transaction *txn, const RID &rid) { ...... latch_.lock(); if (lock_table_.find(rid) == lock_table_.end()) { // INSERTION AND REHASH MAY HAPPENDS HERE! lock_table_[rid].state_ = RIDLockState::UNLOCKED; } std::unique_lock lk(lock_table_[rid].queue_latch_); latch_.unlock(); LockRequest req = MakeRequest(txn->id, LOCK_SHARED); // IF REHASH AND THIS LINE IS RUNNING AT THE SAME TIME, WHAT WILL HAPPEN ? lock_table_[rid].queue_.emplace_back(req); auto req_iter = --lock_table[rid].queue_.end(); // AND WHAT MAY HAPPEND HERE ??? while (wait (lock_table_[rid].queue_.cv_, lk)) .... lk.unlock(); return true; // unlock the queue }错误已经很明显了,我在插入LockRequest前,使用了operator []来获取对应的LockRequestQueue的引用,本意是认为新的RID的插入并不会影响这一过程,但如果在operator []执行的过程中发生了rehash,那么 bucket_num的值就会发生改变(应该是从7变为11),而这个过程中,同一个hashval就可能被送到不同的bucket中,因此就产生了undefined behaviour。
那么这个BUG该如何解决呢?再次阅读cpp reference我们可以得知,rehash不会导致引用或者指针失效,因此我们可以在持有latch_的时候,直接获取到对应的LockRequestQueue的引用,以后只通过这个引用来访问LockRequestQueue,伪代码如下:
bool LockManager::LockShared(Transaction *txn, const RID &rid) { ...... latch_.lock(); if (lock_table_.find(rid) == lock_table_.end()) { // INSERTION AND REHASH MAY HAPPENDS HERE! lock_table_[rid].state_ = RIDLockState::UNLOCKED; } // acquire the reference of LockRequestQueue; LockRequestQueue &queue = lock_table_[rid]; latch_.unlock(); // rehash does not invalid reference queue, so it's safe here. std::unique_lock lk(queue.queue_latch_); LockRequest req = MakeRequest(txn->id, LOCK_SHARED); queue.queue_.emplace_back(req); auto req_iter = --queue.queue_.end(); while (wait (queue.cv_, lk)) .... lk.unlock(); return true; // unlock the queue }还剩下一个问题:在rehash的过程中,引用是否会失效?我个人查了查相关资料,没有找到对应的讨论情况。虽然我个人认为引用不会失效,但很明显我们不应该依靠这种侥幸心理来写代码,后续重构的时候我会像一个更好地方法来解决这个问题。
总结std::unordered_map<Key, Value>是一个无法保证线程安全的数据结构,我们必须自己来处理它的并发访问。并发访问可以支持单个进程的写操作,或者多个进程的并发读操作。一般情况下我们可以把对Value的写操作,看做是一个对std::unordered_map<Key, Value>的读操作,因为这个操作并不改变Key与Value的映射关系。operator[]是一个十分需要小心使用的方法,因为它既可能对应一个读操作,也可能对应一个写操作,如果这个方法触发了插入行为,那么其中的元数据就会被修改,如果装载引子接近了上限值时还可能触发rehash,因此operator []不应该并发的调用。
这算是我个人遇到的最难受的BUG之一了,既没法在本地复现,又没法得到更多的信息。个人非常大的失误是不了解std::unordered_map的行为,并且在BUG定位的时候没能跳出逻辑圈。实际上,试图降低锁粒度对我这种经验很少的鶸来说,是非常危险的行为,因此我个人十分不建议在缺少全局分析、缺乏经验、没有足量的测试的情况下写并发代码,能单机解决它不香嘛(