线程安全向量

Thread safe vector

本文关键字:向量 安全 线程      更新时间:2023-10-16

我首先要说的是,我读过关于这个主题的大多数SO和其他主题。

根据我的理解,std::vector 将在推回新项时重新分配内存,这是我的情况,除非我保留了足够的空间(这是不是我的情况)。

我得到的是std::shared_ptr类型的vector,该vector保存唯一对象(或者更准确地说,是指向vector中唯一对象的指针)。

通过指针对这些对象的处理被封装在一个Factory &处理程序类,但是指向对象的指针可以从包装器类外部访问,并且可以修改成员值。任何时候都不能删除。

如果我正确理解了前面关于std::vector和线程安全的SO问题,添加(push_back)新对象可能会使之前的指针失效,因为vector内部可能会重新分配内存并复制所有内容,这对我来说当然是一场灾难。

我的意图是从vector中读取,通常通过指针修改对象,并从异步运行的线程中向vector中添加新项。

  1. 使用原子或互斥体是不够的?如果我从一个线程推回,另一个线程处理一个对象通过指针可能最终有一个无效的对象?
  2. 是否有一个库可以处理这种形式的MT问题?我一直在阅读的是英特尔的TBB,但由于我已经在使用c++ 11,我希望保持最小的变化,即使这意味着更多的工作,我想在这个过程中学习,而不仅仅是复制粘贴。
  3. 除了在修改对象时锁定访问之外,我希望对vector的异步并行读访问不会被push_backs失效。我怎样才能做到呢?

如果它很重要的话,以上所有内容都是在linux (debian jessie)上使用gcc-4.8并启用c++11。

我对使用微创库持开放态度。

提前感谢:-)

添加(push_back)新对象可能使先前的指针失效…

不,此操作不会使任何先前的指针无效,除非您引用vector内部数据管理中的地址(这显然不是您的场景)。
如果您将原始指针或std::shared_ptr存储在那里,这些指针将被简单地复制,而不会无效。


正如在评论中提到的,std::vector不是很适合保证生产者/消费者模式的线程安全,原因有很多。存储原始指针来引用活动实例都不是!

A Queue将更好地支持这一点。至于标准,您可以使用std::deque为生产者/消费者提供某些接入点 (front(), back())。

为了使这些访问点的线程安全(对于推/弹出值),你可以很容易地用你自己的类包装它们,并使用互斥锁,以确保在共享队列引用上的插入/删除操作安全。另一个(也是主要的,从你的问题来看)点是:管理所包含/引用的实例的所有权和生命周期。您也可以将所有权转移给消费者,如果这适合您的用例(从而摆脱开销与例如std::unique_ptr),参见下面…

<子>此外,您可能有一个信号量(条件变量),以通知消费者线程,新数据可用。


1。使用原子或互斥体是不够的?如果我从一个线程推回,另一个线程处理一个对象通过指针可能最终有一个无效的对象?'

存储到队列(共享容器)的实例的生命周期(因此线程安全使用)需要单独管理(例如使用智能指针,如std::shared_ptrstd::unique_ptr存储在那里)。

2。有图书馆吗?’

使用现有的标准库机制可以很好地实现。

对于第3点。看看上面写的。正如我所能告诉你的,听起来你想要一个类似rw_lock互斥锁的东西。您可以为它提供一个带有合适条件变量的代理。

如果您总是向容器中添加新项然后访问它们,那么您可能会发现使用另一种间接方式的向量是有用的,这样就不会将内部缓冲区交换为更大的缓冲区,一旦分配的空间永远不会被释放,新的空间只是以线程安全的方式添加。

例如,它看起来像这样:

concurrent_vector<Object*>:
  size_t m_baseSize = 1000
  size_t m_size     = 3500
  part*  m_parts[6] = {
    part* part1, ----> Object*[1000]
    part* part2, ----> Object*[2000]
    part* part3, ----> Object*[4000]
    NULL,
    ...
  }

类包含一个固定的指针数组,这些指针指向带有项的单个内存块,其大小呈指数级增长。这里的限制是6个部分,所以63000项-但这可以很容易地改变。

容器开始时,所有部件指针都被设置为NULL。如果添加了一个项,则创建第一个块,大小为m_baseSize,这里为1000,并保存到m_parts[0]。后面的项目写在这里。

当块被填满时,另一个缓冲区被分配,其大小是前一个(2000)的两倍,并存储在m_parts[1]中。根据需要重复此操作。

所有这些都可以使用原子操作完成,但这当然很棘手。如果所有的写操作都可以用互斥锁保护,只有读操作是完全并发的(例如,如果写操作是非常罕见的操作),这可能会更简单。所有读线程总是看到m_parts[i]中的NULL或其中一个缓冲区中的NULL,或者一个有效的指针。已存在的项永远不会在内存中移动,也不会失效。


就现有的库而言,你可能想看看Intel的线程构建块,特别是它的concurrent_vector类。据报道,它有以下功能:
  • 按索引随机访问。第一个元素的索引为0。
  • 多个线程可以并发地扩展容器和添加新元素。
  • 容器的增长不会使现有的迭代器或索引失效。

重新阅读这个问题,情况似乎有点不同。

std::vector不适合存储必须保持引用的对象,因为push_back会使对存储对象的所有引用无效。但是,你正在存储一堆std::shared_ptr

存储在里面的std::shared_ptr s应该优雅地处理大小调整(它们被移动,但不是它们指向的对象),只要在线程中,不保留对存储在vector中的std::shared_ptr s的引用,而是保留它们的副本

使用std::vectorstd::deque都必须同步访问数据结构,因为push_back虽然不是引用无效,但会改变deque的内部结构,因此不允许与deque访问同时运行。

OTOH, std::deque可能出于性能原因更适合;在每次调整大小时,你都要移动很多std::shared_ptr,这可能不得不对复制/删除的情况下的重新计数进行锁定的增量/减量(如果它们被移动-它们应该-这应该被省略-但是YMMV)。

但最重要的是,如果您使用std::shared_ptr只是为了避免向量中的潜在移动,那么在使用deque时可以完全放弃它,因为引用不会无效,因此您可以直接将对象存储在deque中,而不是使用堆分配和std::shared_ptr的间接/开销。