如何使用类似于std::vector<std::mutex>的东西?

43

我有许多对象,可能数量很大,这些对象正在并发写入。我想用互斥锁来保护对它们的访问。为此,我考虑使用std::vector<std::mutex>,但是这不起作用,因为std::mutex没有复制或移动构造函数,而std::vector::resize()需要它们。

有什么推荐的解决方案吗?

编辑: 所有C++随机访问容器是否都需要重新调整大小的复制或移动构造函数?std::deque是否有帮助?

再次编辑:

首先,感谢您的所有思考。我对避免使用互斥锁或将其移动到对象中(我不愿透露详细信息和原因)的解决方案不感兴趣。因此,考虑到我需要可调节数量的互斥锁(在没有互斥锁被锁定时保证进行调整),那么似乎有几种解决方案。

1 我可以使用固定数量的互斥锁,并使用哈希函数将对象映射到互斥锁上(如Captain Oblivous的答案)。这将导致冲突,但如果互斥锁的数量远大于线程数,但仍小于对象数,则冲突的数量应该很少。

2 我可以定义一个包装类(如ComicSansMS的答案),例如

struct mutex_wrapper : std::mutex
{
  mutex_wrapper() = default;
  mutex_wrapper(mutex_wrapper const&) noexcept : std::mutex() {}
  bool operator==(mutex_wrapper const&other) noexcept { return this==&other; }
};

使用std::vector<mutex_wrapper>来管理mutex(互斥锁)。

3 我可以使用std::unique_ptr<std::mutex>来管理单个mutex(就像Matthias的答案中所示)。这种方法的问题在于每个mutex都是在堆上单独分配和释放的。因此,我更喜欢

4 std::unique_ptr<std::mutex[]> mutices( new std::mutex[n_mutex] );

当需要一定数量的mutices时,初始分配一定数量n_mutex, 如果后来发现不足, 我只需简单地

if(need_mutex > n_mutex) {
  mutices.reset( new std::mutex[need_mutex] );
  n_mutex = need_mutex;
}

那么我应该使用这些中的哪一个呢(1、2、4)?


vector 只需要对象可移动,而对于 resize,它们需要是无抛出移动构造的。 - Kerrek SB
3
可以使用 collection<std::atomic<T>> 来替代 collection<T>collection<mutex> 以保护前者。 - Jerry Coffin
2
我觉得你所尝试的方法可能存在更好的解决方案。你能提供一些关于需要使用互斥锁的根本问题的背景吗?你是如何写入对象的,以及你计划如何使用互斥锁?我的第一反应是,你可以通过将互斥锁作为类的成员字段添加到对象中,使其线程安全。 - BTownTKD
@BTownTKD 抱歉,但这会有点冗长...所以我不详细说明。然而,我非常确定使用互斥锁的解决方案是最好的。实际上,原子操作并不是一个好主意,因为库会使用更细粒度的互斥锁来实现它。 - Walter
请注意,向量中的互斥锁也分配在堆上!您可以完全这样做:std::vectorstd::mutex vec(10);同时也可以这样:auto vec = std::vectorstd::mutex(10); - mfnx
显示剩余2条评论
7个回答

23

vector要求其元素可移动,以便在增长时保持连续的值数组。你可以创建一个包含互斥锁的向量,但是你不能做任何可能需要调整大小的事情。

其他容器没有这个要求;使用 deque 或者 [forward_]list 应该可以工作,只要你在构造期间通过就地构造或使用 emplace()resize() 构造互斥锁。诸如 insert()push_back() 等函数将无法工作。

另外,你可以添加额外的间接级别并存储 unique_ptr ;但是你在另一个回答中的评论表明你认为动态分配的额外成本是不可接受的。


1
std::unique_ptr<std::vector<std::mutex>>怎么样?在构造时,向量将使用其元素的默认构造函数,因此没有问题。 - Walter
@Walter:确实,使用构造函数是可以的。我的意思是你需要使用emplace()而不是push_back()来添加单个互斥量,但我没有表达清楚。 - Mike Seymour
2
@Walter:如果你想要调整大小,就不能使用向量;用unique_ptr包装它也没有任何区别。如果你有一个固定的数字,那么一个适当构造的向量应该可以工作;但是你说“可能变化”,这就排除了使用vector的可能性。 - Mike Seymour
我本意并不是想用 resize() 来改变大小,而是想要删除并重新构造整个互斥向量。这应该和使用 resize() 一样快。在这种情况下,我认为使用 unique_ptr 来持有这个向量是最清晰的方式,虽然也许可以原地析构和重构(?). - Walter
1
@Walter:好的,我现在明白你的意思了。那应该可以,但请确保您在任何互斥锁被锁定时都不要这样做。 - Mike Seymour
显示剩余2条评论

21

如果想要创建一定长度:

std::vector<std::mutex> mutexes;
...
size_t count = 4;
std::vector<std::mutex> list(count);

mutexes.swap(list);

3
这个回答确认了vector不需要移动构造项,这取决于你如何使用vector。你可以通过一种方式(仅有的一种方式,即你的“交换”技巧)扩展一个vector<mutex>,但是你可以很容易地缩小这样的向量,并且你可以(如你的回答所示)确保它具有正确的大小。 - Aaron McDaid
我不明白为什么这个答案不是首选?除了更大的开销之外,它在功能上与.resize()有何不同。你所说的“只能单向增长”是什么意思?(如果格式不正确,对不起,我是新来的) - Supamee
4
经过尝试,我现在明白了,如果使用.swap()来扩展向量,那么所有的元素都是新的。 - Supamee
我想知道这是一种实现上的小问题,还是在符合标准的每个C++库中都应该起作用。 - lvella

18

你可以使用std::unique_ptr<std::mutex> 来代替 std::mutexunique_ptr 是可移动的。


2
分别在堆上创建每个互斥锁?你开玩笑吧? - Walter
10
@Walter:如果额外的内存占用和额外层级的间接成本可以接受,那这是一个完全合理的建议。 - Mike Seymour
1
@MikeSeymour和分配和释放许多单独的互斥锁的成本。这是一个可怕的解决方法。实际上,我根本不想复制互斥锁。我只想要默认构造的互斥锁,数量可以改变。不幸的是,vector::resize()无法做到这一点... - Walter
2
@Walter:为什么不创建一个类,使其保持与其对象相关联的互斥锁?然后编写自定义的复制和移动构造函数以及赋值运算符,使您的类可以在容器中正常使用。保留裸露的互斥锁似乎有点愚蠢。 - GManNickG
1
@Walter 在这种情况下,您需要在调整大小时重新创建所有的互斥锁。只有当当前没有任何已持有的互斥锁时,您才能安全地这样做。您能确保这一点吗? - Matthias Benkard
显示剩余2条评论

9

我建议使用固定的互斥池。维护一个固定大小的std::mutex数组,并根据对象的地址选择要锁定的互斥量,就像使用哈希表一样。

std::array<std::mutex, 32> mutexes;

std::mutex &m = mutexes[hashof(objectPtr) % mutexes.size()];

m.lock();
< p > hashof 函数可以是将指针值向左移动几个位的简单操作。这样,您只需要初始化一次互斥量,并避免复制调整向量大小。

有趣的想法。不能保证没有两个objectPtr导致相同的互斥体,从而导致更多的争用。我认为只要互斥体的数量比线程的数量大(多少),这应该是可以的。 - Walter
正确的,你会遇到碰撞。如果这些碰撞不影响你,这可能是一种更有效的替代方案。 - Captain Obvlious
我已经使用了比实际对象和组少得多的互斥量,并将对象分成块,因为每当一个线程必须写入一个对象时,它很可能也会写入附近的对象。 - Walter
这是一个非常棒的想法!它适用于我的情况,即碰撞只会产生最小的影响(只需等待一小段时间,智能指针包含在大向量中被复制、更新或重置)。 - Antonio

3

如果效率是个问题,我猜想你可能只有非常小的数据结构,并且这些结构经常被修改。那么使用原子比较和交换操作(以及其他原子操作),而不是使用互斥锁,特别是 std::atomic_compare_exchange_strong 可能会更好。


1

当我需要一个每个都有自己的 std::mutexclassstructstd::vector 时,我有时会使用类似于您第二个选项的解决方案。当然,这有点繁琐,因为我要编写自己的复制/移动/赋值运算符。

struct MyStruct {
  MyStruct() : value1(0), value2(0) {}
  MyStruct(const MyStruct& other) {
    std::lock_guard<std::mutex> l(other.mutex);
    value1 = other.value1;
    value2 = other.value2;
  }
  MyStruct(MyStruct&& other) {
    std::lock_guard<std::mutex> l(other.mutex);
    value1 = std::exchange(other.value1, 0);
    value2 = std::exchange(other.value2, 0);
  }
  MyStruct& operator=(MyStruct&& other) {
    std::lock_guard<std::mutex> l1(this->mutex), l2(other.mutex);
    std::swap(value1, other.value1);
    std::swap(value2, other.value2);
    return *this;
  }
  MyStruct& operator=(const MyStruct& other) {
    // you get the idea
  }
  int value1;
  double value2;
  mutable std::mutex mutex;
};

您不需要“移动”std::mutex。只需在“移动”其他所有内容时持有其锁定即可。


-4

把每个互斥锁声明为指针怎么样?

std::vector<std::mutex *> my_mutexes(10)
//Initialize mutexes
for(int i=0;i<10;++i) my_mutexes[i] = new std::mutex();

//Release mutexes
for(int i=0;i<10;++i) delete my_mutexes[i];

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接