【问题标题】: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

标签: c++ multithreading memory-barriers


【解决方案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(); }

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

    【讨论】:

      猜你喜欢
      • 2016-08-04
      • 2021-06-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-20
      • 2019-06-18
      相关资源
      最近更新 更多