锁定一对相互引用的资源

时间:2017-05-16 21:03:37

标签: c++ multithreading concurrency synchronization locking

请考虑以下事项:

// There are guys:
class Guy {
    // Each guy can have a buddy:
    Guy* buddy; // When a guy has a buddy, he is his buddy's buddy, i.e:
                // assert(!buddy || buddy->buddy == this);

public:
    // When guys are birthed into the world they have no buddy:
    Guy()
        : buddy{}
    {}

    // But guys can befriend each other:
    friend void befriend(Guy& a, Guy& b) {
        // Except themselves:
        assert(&a != &b);

        // Their old buddies (if any), lose their buddies:
        if (a.buddy) { a.buddy->buddy = {}; }
        if (b.buddy) { b.buddy->buddy = {}; }

        a.buddy = &b;
        b.buddy = &a;
    }

    // When a guy moves around, he keeps track of his buddy
    // and lets his buddy keep track of him:
    friend void swap(Guy& a, Guy& b) {
        std::swap(a.buddy, b.buddy);
        if (a.buddy) { a.buddy->buddy = &a; }
        if (b.buddy) { b.buddy->buddy = &b; }
    }
    Guy(Guy&& guy)
        : Guy()
    {
        swap(*this, guy);
    }
    Guy& operator=(Guy guy) {
        swap(*this, guy);
        return *this;
    }

    // When a Guy dies, his buddy loses his buddy.
    ~Guy() {
        if (buddy) { buddy->buddy = {}; }
    }
};

到目前为止一切都很顺利,但现在我希望这可以在不同的线程中使用好友时使用。没问题,让我们在std::mutex中坚持Guy

class Guy {
    std::mutex mutex;

    // same as above...
};

现在,我必须在链接或取消链接这两者之前锁定两个人的互斥锁。

这是我难倒的地方。以下是失败的尝试(以析构函数为例):

  1. 死锁:

    ~Guy() {
        std::unique_lock<std::mutex> lock{mutex};
        if (buddy) {
            std::unique_lock<std::mutex> buddyLock{buddy->mutex};
            buddy->buddy = {};
        }
    }
    

    当两个好友大约在同一时间被摧毁时,他们每个人都可能会锁定他们自己的互斥锁,然后才能锁定他们的好友。互斥,从而导致僵局。

  2. 比赛条件:

    好的,我们只需要按照一致的顺序锁定互斥锁,无论是手动还是std::lock

    ~Guy() {
        std::unique_lock<std::mutex> lock{mutex, std::defer_lock};
        if (buddy) {
            std::unique_lock<std::mutex> buddyLock{buddy->mutex, std::defer_lock};
            std::lock(lock, buddyLock);
            buddy->buddy = {};
        }
    }
    

    不幸的是,为了获得好友的互斥锁,我们必须访问buddy,此时此版本不受任何锁的保护,并且可能正在从另一个线程进行修改,这是一场比赛条件。

  3. 不可扩展:

    使用全局互斥锁可以

    static std::mutex mutex;
    ~Guy() {
        std::unique_lock<std::mutex> lock{mutex};
        if (buddy) { buddy->buddy = {}; }
    }
    

    但出于性能和可扩展性的原因,这是不可取的。

  4. 这可能没有全局锁定吗?怎么样?

1 个答案:

答案 0 :(得分:2)

使用std::lock不是竞争条件(本身)也不存在死锁风险。

std::lock将使用无死锁算法来获取两个锁。它们将是某种(未指明的)尝试和撤退方法。

另一种方法是确定任意锁定顺序,例如使用对象的物理地址。

您已经正确地排除了对象伙伴本身的可能性,因此没有尝试lock()同一mutex两次的风险。

我说这不是一个竞争条件本身,因为该代码将做的是确保完整性,如果有一个伙伴b,那么b为所有a和b都有伙伴a。

事实上,在与两个对象建立联系之后,他们可能会被另一个线程感到不友好,这可能是您打算或通过其他同步解决的问题。

另请注意,当您与朋友结交并且可能与新朋友的朋友不友好时,您需要立即锁定所有对象。 这是两个&#39;朋友和他们现在的朋友(如果有的话)。 因此,您需要锁定2,3或4个互斥锁。

std::lock遗憾的是没有接受数组,但有一个版本可以在boost中执行,或者您需要手动解决它。

为了澄清,我正在阅读可能的析构函数的示例作为模型。所有相关成员都需要对同一锁定进行同步(例如befriend()swap()unfriend()(如果需要)。实际上,锁定2,3或4的问题适用于befriend()成员。

此外,析构函数可能是最糟糕的例子,因为正如评论中所提到的那样,一个对象是可破坏的,但可能与另一个线程发生锁争用是不合逻辑的。在更广泛的程序中肯定需要存在一些同步,以使得不可能在这一点上析构函数中的锁是多余的。

确实在销毁之前确保Guy个对象没有伙伴的设计似乎是一个好主意和一个在析构函数中检查assert(buddy==nullptr)的调试前置条件。遗憾的是,它不能作为运行时异常,因为在析构函数中抛出异常会导致程序终止(std::terminate())。

事实上,真正的挑战(可能取决于周围的计划)是如何在交友时取消联系。这似乎需要一个尝试撤退循环:

  1. 锁定a和b。
  2. 了解他们是否有朋友。
  3. 如果他们已经是好友 - 你已经完成了。
  4. 如果他们有其他伙伴,请解锁&amp; b,并锁定a和b以及他们的伙伴(如果有的话)。
  5. 检查好友是否已经改变,如果他们又回来了。
  6. 调整相关成员。
  7. 对于周围的程序来说,这是一个存在实时锁定风险的问题,但任何尝试撤退方法都会遭受同样的风险。

    std::lock() a&amp; b然后std::lock()好友,因为这确实存在死锁风险。

    所以回答这个问题 - 是的,没有全局锁定是可能的,但这取决于周围的程序。它可能在许多Guy个对象的群体中,争用很少,而且活力很高。 但可能是有少量对象热议(可能在大量人群中)导致问题。如果不了解更广泛的应用,就无法对其进行评估。

    解决这个问题的一种方法是锁定升级,这实际上是零碎的回归到全局锁定。本质上,这意味着如果在重试循环周围有太多的行程,则将设置全局信号量,命令所有线程进入全局锁定模式一段时间。一段时间可能是一些行动或一段时间或直到全球锁定的争论消退!

    所以最后的答案是&#34;是的,它是绝对可能的,除非它在哪种情况下都不起作用&#39; no&#39;&#34;。