记一个关于std::unordered_map并发访问的BUG (2)

目前你只需要知道这个,为了同步多个线程(事务)的并发访问,线程会调用单例的LockManager中的lock_shared、lock_exclusive、lock_upgrade方法来获取锁,而锁的粒度是RID级别的就可以了。Project4 Part1的要求就是让我们实现上述三个方法。

设计与实现

具体的实现思路不难。我们先看一下LockManager的核心数据结构lock_table_,它实现了从RID到LockRequestQueue的映射,所有的事务在为某个RID申请lock时,必须将自己的请求构建一个LockRequest结构,插入到这个RID对应的LockRequestQueue里面,等待自己的请求被执行。图例如下:

记一个关于std::unordered_map并发访问的BUG

本图中共有5个RID,理所应当有5个LockRequestQueue,这里为了简便起见我只画了其中的两个。上图中有四个事务(1,2,7,4)申请RID 1的锁,三个事务(3,2,1)申请RID 4的锁,请求到来的顺序、授予锁的顺序都是FIFO。由于RID 1的前三个请求都是S-LOCK,因此这把S-LOCK可以被授予给三个事务,而第四个事务(txn 4)的X-LOCK请求则被阻塞;

能画出这个图之后,相应的代码也不难实现了,这里我以伪代码的形式给出lock_shared的逻辑,lock_exclusive和lock_upgrade的逻辑都很类似,故不再给出,我所想讨论的BUG也在下面的代码里面:

bool LockManager::LockShared(Transaction *txn, const RID &rid) { // Assertions for transaction state, isolation level and so on. ...... // BUG IS HERE!!!!!! latch_.lock(); if (lock_table_.find(rid) == lock_table_.end()) { // INSERTION MAY HAPPENDS HERE! lock_table_[rid].state_ = RIDLockState::UNLOCKED; } // block other txns' visit on this rid,then we could saftly release latch_ std::unique_lock lk(lock_table_[rid].queue_latch_); latch_.unlock(); // BUG IS HERE!!!! LockRequest req = MakeRequest(txn->id, LOCK_SHARED); lock_table_[rid].queue_.emplace_back(req); auto req_iter = --lock_table[rid].queue_.end(); // waiting on condition_variable while (wait(lock_table_[rid].queue_.cv_, lk)) .... // Success, return lk.unlock(); return true; // unlock the queue }

lock_table_是一个std::unordered_map<RID, LockRequestQueue>,最开始的时候是空的,当出现一个新的RID时,会向这个unordered_map中添加一对新的 (RID, LockRequestQueue)的Pair。这是一个对unordered_map的写操作,而unordered_map并不是线程安全的,如果多个线程同时调用lock_shared去获取同一个RID的锁,而这个RID之前并不在lock_table_里,那么lock_table可能就会并发的插入同一个RID,这是非常危险的操作!!!我们必须避免这一情景,通常的方法是在可能引入插入的操作之前,获取latch_来保证单线程对lock_table_的访问操作

// BUG IS HERE!!!!!! latch_.lock(); if (lock_table_.find(rid) == lock_table_.end()) { // INSERTION MAY HAPPENDS HERE! lock_table_[rid].state_ = RIDLockState::UNLOCKED; } // block other txns' visit on this rid,then we could saftly release latch_ std::unique_lock lk(lock_table_[rid].queue_latch_); latch_.unlock();

正如前文所述,由于operator []的使用可能会引入插入操作,因此这里我用latch_.lock()来隔绝其他线程对lock_table_的访问操作,这样可以避免多个线程对同一个RID同时进行插入操作的情景,当lock_table_[rid].state_ = RIDLockState::UNLOCKED;这行代码执行完毕后,这个RID的KV Pair已经存放在了这个unordered_map里面,然后我们拿到更细粒度的锁lock_table_[rid].queue_latch_,就可以释放掉latch_了,因为虽然后续的代码会访问lock_table_里的LockRequestQueue,但由于对应的RID均已经存在,因此这些操作应该看做是对lock_table_的读操作;虽然这个过程中会有新的RID插入,但不应该会对这些已经存在的RID产生影响(这里是我的第一个疏忽),因此无需关注。而那些针对LockRequestQueue的读写操作,由更加细粒度的锁LockRequestQueue.queue_latch_来提供同步,而其在latch_释放之前就已经被获取了,因此也不存在并发问题。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwfdpw.html