通过检查条件并重新检查来获取锁

Posted

技术标签:

【中文标题】通过检查条件并重新检查来获取锁【英文标题】:Acquiring lock by checking against a condition and rechecking it 【发布时间】:2018-05-29 06:36:18 【问题描述】:

这样的事情是否有效:

std::vector<std::vector<int>> data;
std::shared_mutex m;
...

void Resize() 
    // AreAllVectorsEmpty: std::all_of(data.begin(), data.end(), [](auto& v)  return v.empty(); 
    if (!AreAllVectorsEmpty()) 
        m.lock();
        if (!AreAllVectorsEmpty()) 
            data.resize(new_size);
        
        m.unlock();
    

我正在检查AreAllVectosEmpty(),然后如果条件成功,则获取锁定,然后再次检查相同的条件是否进行调整大小。

这会是线程安全的吗? Resize 只被一个线程调用,其他线程操作data 的元素。

AreAllVectorsEmpty 是否要求有内存栅栏或获取语义?

编辑:当m.lockResize 获取时,其他线程当然会阻塞。

编辑:我们还假设new_size 足够大,可以进行重新分配。

编辑:更新 shared_mutex 的代码。

编辑:AreAllVectorsEmtpy 正在迭代数据向量。没有其他人修改数据向量,但数据[0]、数据[1]等被其他线程修改。我的假设是因为 data[0] 的 size 变量在向量内并且是一个简单的整数,因此可以安全地访问 data[0].size()、data[1].size() 等... Resize 线程。 AreAllVectorsEmpty 正在迭代 data 并检查 vector.empty()

【问题讨论】:

如果这个函数不修改共享资源,它是线程安全的 @mangusta 它正在修改data 向量。 m.resize?不是data.resize @WernerHenze 已修复。谢谢。 不要调用mutex.lock(),而是使用std::unique_lock 【参考方案1】:

我会使用 shared_mutex 并使用:

只读取向量(同时读取向量)的所有线程中的共享锁 调整矢量大小时此线程中的唯一锁

我认为首先检查大小,然后调整大小是安全的,前提是这是修改向量内容的唯一线程。

锁自动意味着内存屏障,否则锁没有多大意义。

【讨论】:

我正在使用shared_mutex。为了示例的简单性,我将其更改为mutex。无论如何,是的 Resize 被一个线程调用。【参考方案2】:

答案完全取决于AreAllVectorsEmpty 的实现方式。

如果它只是检查一个可以自动设置的标志,那么是的,它是安全的。如果它遍历您打算更改的向量(或其他常用容器),那么不,它是不安全的(如果向量在内部重新分配,迭代器会发生什么???)。

如果是后者,你需要一个读/写锁机制,看看shared mutexes。

然后您将在检查之前获取共享锁,在修改的情况下获取排他锁。

请注意,如果areAllVectorsEmpty 使用一些独立的数据结构(除了提到的原子标志之外),您可能还必须使用单独的互斥锁来保护它以及

【讨论】:

它正在遍历数据向量。没有其他人修改数据向量,但数据[0]、数据[1]等被其他线程修改。我的假设是因为 data[0] 的 size 变量在向量内部并且是一个简单的整数,因此可以安全地访问 data[0].size()、data[1].size() 等... Resize 线程。 AreAllVectorsEmpty 正在迭代 data 并检查 vector.empty() 好吧,那么:如果你不保护 data 本身的迭代,任何线程都可能获得锁另一个线程仍然迭代 - 然后调整大小可能会使后者的迭代器无效!另外:请注意std::vector::size 通常实现为end() - begin(),而std::vector::empty 再次实现为size() == 0。您不能依赖它们是线程安全的,因此您甚至可能必须适当地保护内部向量免受并发读/写冲突! 调整数据向量的大小仅由一个线程完成,该线程已调用 Resize。问题上也提到了 良好的一般建议:永远不要依赖假设,如果它们不正确,您可能会失败! 一些例子:向量中有4个元素,线程A开始迭代并到达元素0和1。然后线程B中断A,并且由于读取不受保护,可以获得锁。当然,它是现在唯一进行修改的线程,但线程 A 正处于其迭代的中间!它的迭代器会发生什么?【参考方案3】:

标准似乎没有要求这样做,比较http://en.cppreference.com/w/cpp/container#Thread_safety。如果它适用于您的特定编译器和 STL?您需要查看来源。但我不会依赖它。

这让我想到了一个问题:你为什么要这样做?出于性能原因?你测量过性能吗?在调用AreAllVectorsEmpty 之前锁定是否真的对性能造成了可衡量的影响?

顺便说一句,请不要直接锁定互斥锁,请使用std::lock_guard。

【讨论】:

【参考方案4】:

// AreAllVectorsEmpty: std::all_of(data.begin(), data.end(), [](auto& v) 返回 v.empty();

您正在访问内部向量的内部(调用空),同时另一个线程可以将一些元素插入到其中一个内部向量中 -> 数据竞争

【讨论】:

以上是关于通过检查条件并重新检查来获取锁的主要内容,如果未能解决你的问题,请参考以下文章

redisTemplate通过setNx实现分布式锁

第十四章 构建自定义的同步工具

在用户登录时,检查具有相同网站的其他标签并重新加载这些标签

如何以小块上传带有ajax的文件并检查失败,重新上传失败的部分。

如果找不到则检查进程并重新启动程序的脚本[重复]

检查用户代理并在会话类中重新生成会话 ID