线程安全矢量

时间:2014-05-12 23:26:23

标签: c++ multithreading c++11 vector

首先我要说的是,我已经阅读了关于这个主题的大部分SO和其他主题。

我理解事物的方式,std :: vector 在推回新项目时重新分配内存,这是我的情况,除非我保留了足够的空间(这是我的情况。)

我所拥有的是std :: shared_ptr的向量,并且该向量包含唯一对象(或者更准确地说,指向向量中唯一对象的指针)。

通过指针处理这些对象是围绕工厂和工厂进行的。 Handler类,但可以从包装类外部访问指向对象的指针,并且可以修改成员值。任何时候都不会发生删除。

如果我正确理解先前关于std :: vector和线程安全的SO问题中提出的问题,添加(push_back)新对象可能使先前的指针无效,因为向量内部可能会重新分配内存和副本一切都结束了,这对我来说当然是一场灾难。

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

所以,

  1. 使用原子或互斥是不够的?如果我从一个线程回退,另一个通过指针处理对象的线程可能最终会出现一个无效对象?
  2. 是否有可以处理这种形式的MT问题的库?我一直在阅读的是英特尔的TBB,但由于我已经在使用C ++ 11,我很乐意将更改保持在最低限度,即使这对我来说意味着更多的工作 - 我想在这个过程中学习,而不仅仅是复制粘贴。
  3. 除了在修改对象时锁定访问权限,我希望对向量的异步并行读取访问不会被push_backs无效。我怎样才能做到这一点?
  4. 如果它具有任何重要性,以上所有内容都是在linux(debian jessie)上使用gcc-4.8并启用了c ++ 11。

    我愿意使用微创图书馆。

    非常感谢提前: - )

3 个答案:

答案 0 :(得分:7)

  

添加(push_back)新对象可能会使先前的指针无效......

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


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

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

要使这些访问点的线程安全(用于推送/弹出值),您可以轻松地使用自己的类包装它们并使用互斥锁来保护共享队列引用上的插入/删除操作。
另一个(和主要的,从您的问题)点是:管理包含/引用的实例的所有权和生命周期。您也可以将所有权转移给消费者,如果这适用于您的使用案例(因此使用例如std::unique_ptr开销获得),请参阅下文...

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


  

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

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

  

“2。有图书馆......'

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

至于第3点。看看上面写的是什么。正如我可以进一步说明的那样,听起来你要求像rw_lock互斥量这样的东西。您可以使用合适的condition variable为此提供代理。

随意请求更多澄清......

答案 1 :(得分:3)

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

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

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

使用std::shared_ptrstd::vector都必须同步对数据结构的访问,因为std::deque虽然不是引用无效,但会改变{{1}的内部结构因此不允许与双端队列访问同时运行。

OTOH,push_back可能因性能原因而更适合;在每次调整大小时,你正在移动很多deque,这可能需要在复制/删除的情况下对引用计数进行锁定增量/减量(如果它们被移动 - 它们应该 - 这应该被省略 - 但是YMMV)。

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

答案 2 :(得分:3)

如果你总是只是向容器中添加新项目然后访问它们,你会发现有用的是带有另一个间接的向量,因此不是将内部缓冲区交换为更大的内容,而是一次分配的空间是永远不会释放,只是以线程安全的方式添加新空间。

例如,它看起来像这样:

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或有效指针。现有项目永远不会在内存中移动,无效或任何内容。


就现有图书馆而言,您可能希望英特尔查看Thread Building Blocks,特别是其类concurrent_vector。据说它有这些功能:

  • 按索引随机访问。第一个元素的索引为零。
  • 多个线程可以使容器增长并同时追加新元素。
  • 增长容器不会使现有的迭代器或索引无效。