【问题标题】:storing mutexes in a vector/deque c++将互斥体存储在向量/双端队列中 C++
【发布时间】:2015-05-24 00:09:13
【问题描述】:

我想将可变数量的互斥锁存储在一个容器中,比如向量或双端队列。

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

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

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:错误:使用已删除 函数‘std::lock_guard<_mutex>::lock_guard(const std::lock_guard<_mutex>&) [with _Mutex = std::mutex]'

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

删除表达式将调用析构函数(如果有) 被删除的对象或数组的元素。在一个 数组,元素将按地址递减顺序销毁 (也就是说,按照它们的构造函数完成的相反顺序;参见 12.6.2)。

这种方法有什么潜在的缺陷吗?如何让它发挥作用?

编辑

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

编辑2

Q:如果用例是:

所有互斥锁都由不同的线程以任意顺序锁定/解锁(但是每个线程一次只使用 1 个互斥锁), 但有时我需要在另一个线程中以安全的方式锁定所有互斥锁。

【问题讨论】:

  • lock_guard 不能移动(也不能复制)。 unique_lock OTOH 可以移动。
  • 编译错误不是抱怨 lock_guard 不是可复制构造的吗?这与删除顺序无关。 lock_guard 删除了它的复制构造函数并且没有定义移动构造函数,所以你不能将它移动到向量中。见en.cppreference.com/w/cpp/thread/lock_guard
  • @Appleshell 我不太明白为什么要调用复制构造函数,我明确告诉移动警卫而不是复制
  • @Appleshell 关于删除顺序的声明是说可能存在死锁。我说“我猜可能还有问题……”
  • @iggy 定义(或删除)复制构造函数时,不会生成移动构造函数。 lock_guard 删除了它的拷贝构造函数,因此既没有移动也没有拷贝构造函数。

标签: c++11 mutex


【解决方案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&lt;T&gt;::emplace_back 要求T 可以移动构造。这就是unique_lock 在这里工作而lock_guard 不工作的原因之一。 mutexes 无需持有不可移动的互斥体,因为它一次性构造了vector,而不是使用emplace_back 添加到它。

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

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

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

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

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

插头

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

Dining Philosophers Rebooted

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

【讨论】:

  • 霍华德,谢谢你的解释!我在问题中添加了更多细节,您能对此发表评论吗?
  • 你关于上限的建议让我想到了 std::array of unique_locks 并使用这个技巧将它们作为可变数量的参数传递给 std::lock:stackoverflow.com/questions/16834851/…
  • 对于 Edit2 它是一个但不是最理想的方式,每个线程确实需要一次处理一个互斥锁,但是当它锁定时,具有较低互斥锁索引的线程基本上被阻塞.
  • 我不确定我是否理解。如果需要锁定的互斥锁的子集在需要时没有解锁,每个线程都会阻塞(无论互斥锁的顺序如何,或者对互斥锁的顺序引用发送到std::lock)。
  • 假设我们有线程 t1, t2 相应地(并且分别)使用 m1, m2,并且有一个 t3 需要同时锁定 m1 和 m2。因此,要使 t2 工作,它需要使用您的方法阻止 t1,尽管理想情况下它不必这样做(我不确定理想情况是否可以实现)。
【解决方案2】:

可以用new构造lock_guards并放入unique_ptrs吗?

那么向量将持有std::unique_ptr&lt;std::lock_guard&lt;std::mutex&gt;&gt; 而不仅仅是std::lock_guard&lt;std::mutex&gt;

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));
}

应该可以正常编译。

【讨论】:

  • 正如我所说,编译不是唯一的问题。您的代码可能会导致死锁
  • 但实际上在代码中将向量更改为 std::array 可以解决问题
  • 如果锁被其他线程以任何顺序锁定/解锁,那么这将不起作用。然后,您将需要建立一个已定义的锁定顺序并在所有线程中遵循它。
  • std::lock 提供无死锁锁定功能,但我认为它不能在向量/范围上工作。 Boost 似乎有一个版本可以做到这一点:boost.org/doc/libs/1_58_0/doc/html/thread/…
  • 对不起,即使是我的 Edit2 也没有把问题说清楚。如果没有同时锁定所有锁的每个线程都使用单个锁,那么您的方法 + 数组将起作用,否则 - 否
猜你喜欢
  • 1970-01-01
  • 2018-05-18
  • 2013-03-14
  • 2015-05-01
  • 1970-01-01
  • 2012-09-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多