在 vector/deque c++ 中存储互斥量

storing mutexes in a vector/deque c++

我想在 vector 或 deque 等容器中存储可变数量的互斥体。

在其中一个用例中,我需要可靠且无死锁地锁定所有互斥锁。我还想有异常安全保证,如果抛出异常,所有的互斥锁就好像没有发生锁定一样。

我正在尝试做类似的事情:

std::vector<std::mutex> locks_(n);
std::vector<std::lock_guard<std::mutex> > guards(n);
for(int i = 0; i < n; i++) {
    std::lock_guard<std::mutex> guard(locks_[i]);
    guards.emplace_back(std::move(guard));
}

但它没有编译,给我:

/usr/include/c++/4.8/ext/new_allocator.h:120:4: error: use of deleted function ‘std::lock_guard<_Mutex>::lock_guard(const std::lock_guard<_Mutex>&) [with _Mutex = std::mutex]’

我想当lock_guards销毁时也可能有问题,因为与构造相比顺序必须颠倒,但标准为我们节省了:

The delete-expression will invoke the destructor (if any) for the object or the elements of the array being deleted. In the case of an array, the elements will be destroyed in order of decreasing address (that is, in reverse order of the completion of their constructor; see 12.6.2).

这种方法是否存在任何潜在的缺陷,如何才能使其发挥作用?

编辑

其实我错了,好像vector并不能保证特定的破坏顺序。看到这个问题:Order of destruction of elements of an std::vector

编辑2

Q:如果用例是:

所有互斥量都是 locked/unlocked 由不同线程以任何顺序排列(但是每个线程一次只使用 1 个互斥量), 但在某些时候我需要在另一个线程中以安全的方式锁定所有互斥量。

n 上有一个坚定而低的上限,你可以合理地做这样的事情:

#include <iostream>
#include <mutex>
#include <vector>

int
main()
{
    constexpr unsigned n_max = 5;
    unsigned n;
    std::cout << "Enter n: ";
    std::cin >> n;
    if (std::cin.fail())
        throw "oops";
    if (n > n_max)
        throw "oops";
    std::vector<std::mutex> mutexes(n);
    std::vector<std::unique_lock<std::mutex>> locks;
    for (auto& m : mutexes)
        locks.emplace_back(m, std::defer_lock);
    switch (locks.size())
    {
    case 0:
        break;
    case 1:
        locks.front().lock();
        break;
    case 2:
        std::lock(locks[0], locks[1]);
        break;
    case 3:
        std::lock(locks[0], locks[1], locks[2]);
        break;
    case 4:
        std::lock(locks[0], locks[1], locks[2], locks[3]);
        break;
    case 5:
        std::lock(locks[0], locks[1], locks[2], locks[3], locks[4]);
        break;
    default:
        throw "oops";
    }
}

不是那么漂亮。但它易于推理,因此可靠。

备注:

  1. 您需要使用std::lock(m1, m2, ...)来可靠地锁定多个mutex,或者重新发明一种算法,例如std::lock来避免死锁。一种这样的替代算法是,如果你能保证每个人总是以相同的顺序(比如按索引)锁定 mutexes 中的互斥量,那么你根本不需要 std::lock,只需循环并锁定`em.

  2. lock_guard 一次放入 vector 是有问题的,因为 vector<T>::emplace_back 需要 T 才能移动构造。这就是 unique_lock 在这里工作而 lock_guard 不工作的原因之一。 mutexes 摆脱持有不可移动的互斥锁,因为它一次构造了 vector 而不是用 emplace_back.

  3. 添加到它
  4. 在此示例中,locks 包含对 mutexes 的引用。确保这两个容器之间没有生命周期问题(mutexes 必须比 locks 长)。

  5. 如果您需要将不可移动的项目添加到序列的末尾,请切换到 deque,这将在 vector 不起作用的地方起作用。

  6. 解锁顺序无所谓,不用担心。仅当不同的线程可能以不同的顺序锁定时,锁定顺序才重要。如果所有线程总是以相同的顺序加锁,不用担心。但是如果所有线程总是以相同的顺序锁定,请考虑用单个互斥锁替换 n 个互斥锁,因为这听起来是等价的。

  7. 上面的代码假设不同的线程可能以不同的顺序锁定,并且可能是 mutexes 的一个子集。显然它不会扩展到大 n.

问题中有 Edit2,我相信这段代码是可行的。它将可靠地与不同的线程一起工作,以不同的顺序锁定 mutexes。每个线程都应该形成自己的 locks 本地副本并通过 switch 发送。如果某个线程出于某种原因需要其 locks 成为 mutexes 的子集,或者以不同的顺序构建它,没问题。这就是此解决方案的设计目的。

插件

如果您对 std::lock 背后的算法感兴趣,这里有它的各种潜在实现的性能测试,包括您可以 运行 在您自己的平台上的测试代码:

Dining Philosophers Rebooted

如果您发现 std::lock 的实现不是最理想的,请与您的实现者谈谈。 :-)

可以用new构造lock_guards然后放到unique_ptr中吗?

然后向量将保持 std::unique_ptr<std::lock_guard<std::mutex>> 而不是 std::lock_guard<std::mutex>:

std::vector<std::mutex> locks_(n);
std::vector<std::unique_ptr<std::lock_guard<std::mutex>>> guards(n);
for (int i = 0; i < n; i++) {
  typedef std::lock_guard<std::mutex> LockGuardType;
  std::unique_ptr<LockGuardType> guard(new LockGuardType(locks_[i]));
  guards.emplace_back(std::move(guard));
}

编译应该没问题。