我首先要说的是,我读过关于这个主题的大多数SO和其他主题。
根据我的理解,std::vector 将在推回新项时重新分配内存,这是我的情况,除非我保留了足够的空间(这是不是我的情况)。我得到的是std::shared_ptr类型的vector,该vector保存唯一对象(或者更准确地说,是指向vector中唯一对象的指针)。
通过指针对这些对象的处理被封装在一个Factory &处理程序类,但是指向对象的指针可以从包装器类外部访问,并且可以修改成员值。任何时候都不能删除。
如果我正确理解了前面关于std::vector和线程安全的SO问题,添加(push_back)新对象可能会使之前的指针失效,因为vector内部可能会重新分配内存并复制所有内容,这对我来说当然是一场灾难。
我的意图是从vector中读取,通常通过指针修改对象,并从异步运行的线程中向vector中添加新项。
- 使用原子或互斥体是不够的?如果我从一个线程推回,另一个线程处理一个对象通过指针可能最终有一个无效的对象? 是否有一个库可以处理这种形式的MT问题?我一直在阅读的是英特尔的TBB,但由于我已经在使用c++ 11,我希望保持最小的变化,即使这意味着更多的工作,我想在这个过程中学习,而不仅仅是复制粘贴。
- 除了在修改对象时锁定访问之外,我希望对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_ptr
或std::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::vector
和std::deque
都必须同步访问数据结构,因为push_back
虽然不是引用无效,但会改变deque
的内部结构,因此不允许与deque访问同时运行。
OTOH, std::deque
可能出于性能原因更适合;在每次调整大小时,你都要移动很多std::shared_ptr
,这可能不得不对复制/删除的情况下的重新计数进行锁定的增量/减量(如果它们被移动-它们应该-这应该被省略-但是YMMV)。
但最重要的是,如果您使用std::shared_ptr
只是为了避免向量中的潜在移动,那么在使用deque
时可以完全放弃它,因为引用不会无效,因此您可以直接将对象存储在deque
中,而不是使用堆分配和std::shared_ptr
的间接/开销。