对在 std::map 的查找/插入上使用可升级锁感到困惑

confusion over using upgradable lock on std::map's find/insert

本文关键字:可升级 map std 查找 插入 对在      更新时间:2023-10-16

考虑一个线程安全的getter方法,它的形式相对来说是最简单的:

std::map<std::string, boost::shared_ptr<AAA> > repo;
AAA & get(const std::string &key)
{
    boost::upgrade_lock<boost::shared_mutex> lock(repoMutex);
    std::map<std::string, boost::shared_ptr<AAA> >::iterator it = repo.find(key);
    if(it == repo.end()){       
        boost::upgrade_to_unique_lock<boost::shared_mutex> uniqueLock(lock);
        boost::shared_ptr<AAA> t(new AAA(key));
        repo.insert(std::make_pair(key,t));
        return *t;
    }
    return *it->second;
}

在上面,我使用共享(可升级)锁来保护查找操作,并且只有在需要插入密钥/值时才升级到唯一锁。到目前为止还不错吗?

我的想法如下(如果在任何步骤中我的概念是错误的,请告诉我):

  1. 两个线程进入方法

  2. 允许两者同时为同一个密钥运行repo.find()(并且该密钥不存在)。

  3. 两个都失败了。因为密钥不存在。

  4. 第一个线程通过进入升级区域获得独占访问,而等待进入升级区域的第二个

  5. 第一个线程完成为key创建新条目的工作,然后离开。

  6. 第二个线程进入,覆盖第一个线程插入的键/值。(这不是任何人想要的)

我们如何解决这个问题?感谢

简而言之,您当前的解决方案几乎没有任何问题。

首先,第二个线程不会覆盖第一个线程写入的数据,因为map::insert()只插入新的键。您只需要检查insert是否真的插入了元素并返回相应的值。

唯一令人担忧的是可能不必要地免费创建t。在这种情况下,您可以在锁定后添加另一个检查:

std::map<std::string, boost::shared_ptr<AAA> >::iterator it = repo.find(key);
if(it == repo.end()){       
    boost::upgrade_to_unique_lock<boost::shared_mutex> uniqueLock(lock);
    if (repo.find(key) == repo.end() {
        ...
    }
}

但是你应该对你的代码进行评测,看看这是否给你带来了优势。

此外,您可以使用带有提示的map::insert()来避免重复搜索密钥:

std::map<std::string, boost::shared_ptr<AAA> >::iterator it = repo.find(key);
if(it == repo.end()){       
    boost::upgrade_to_unique_lock<boost::shared_mutex> uniqueLock(lock);
    it = repo.lower_bound(key);
    if (it->first != key)
        boost::shared_ptr<AAA> t(new AAA(key));
        repo.insert(it, std::make_pair(key,t));
        return *t;        
    } else {
        return *it->second;
    }
}