☰
oneTBB concurrent_vector 生命周期管理:构造、析构、复制与赋值全面解析
2026/10/9 1:09:19 网站建设 项目流程
  • 并发编程
  • 高性能计算

【免费下载链接】oneTBB

oneAPI Threading Building Blocks (oneTBB)

项目地址:https://gitcode.com/gh_mirrors/on/oneTBB
点击查看免费下载

本文是 oneTBB(oneAPI Threading Building Blocks)容器系列的技术指南,围绕concurrent_vector的构造、析构、复制、移动与赋值这一完整生命周期进行讲解。concurrent_vector是一类可被并发增长和并发访问的动态数组容器,其生命周期语义直接决定了多线程下数据初始化的安全性与资源管理的正确性。读完本文,你将掌握该容器全部构造/赋值重载的语义差异、分配器(allocator)在复制与移动中的传播规则、异常安全保证,以及如何结合源码与测试验证这些行为。

一、认识 concurrent_vector:一个可并发增长的动态数组

concurrent_vector定义于头文件<oneapi/tbb/concurrent_vector.h>,是类模板:

template <typename T, typename Allocator = cache_aligned_allocator<T>> class concurrent_vector;

其类模板 Synopsis 详见 concurrent_vector_cls.rst:默认分配器为tbb::cache_aligned_allocator<T>,以保证元素按缓存行对齐、减少多核竞争下的伪共享(false sharing)。

从源码结构看,concurrent_vector私有继承自segment_table<T, Allocator, concurrent_vector<T, Allocator>, embedded_table_num_segments>(concurrent_vector.h),即底层采用**分段表(segment table)**结构存储元素。embedded_table_num_segments = 3(concurrent_vector.h)表示前 3 个段的指针内嵌于对象本体,后续段通过allocate_long_table动态扩展(concurrent_vector.h)。这种结构决定了本篇文章的生命周期语义:构造负责建立分段表与初始元素,增长操作(grow_by/grow_to_at_least/push_back)负责并发追加元素,析构负责逐元素销毁并归还全部存储。

与std::vector不同,concurrent_vector允许多个线程同时追加元素而不需要外部加锁,但对同一个对象的构造、析构、赋值等"结构性修改"操作,文档明确规定不得与其他操作并发执行(见下文各节反复出现的 "behavior is undefined in case of concurrent operations" 约束)。

二、空容器构造:从零开始的生命起点

默认构造函数

concurrent_vector();

构造一个空的concurrent_vector。源码实现为委托给分配器版构造函数(concurrent_vector.h):

concurrent_vector() : concurrent_vector(allocator_type()) {}

指定分配器的构造函数

explicit concurrent_vector( const allocator_type& alloc ) noexcept;

构造空容器,并使用alloc分配内存。注意该重载被标记为noexcept(concurrent_vector.h),因为此时尚未分配任何元素存储,仅保存分配器,不会抛异常。explicit关键字防止了隐式转换,避免一个分配器被意外当成元素数量。

分配器决定后续所有元素内存的分配方式;oneTBB 提供的tbb::cache_aligned_allocator、tbb::scalable_allocator(见 scalable_allocator.h)均可作为Allocator参数传入。

三、从元素序列构造:批量初始化容器

count 个指定值的副本

explicit concurrent_vector( size_type count, const value_type& value, const allocator_type& alloc = allocator_type() );

构造一个包含count个value副本的concurrent_vector。源码通过grow_by(count, value)实现(concurrent_vector.h),即"先构造空容器,再追加 count 个 value 拷贝"。

count 个就地默认构造的元素

explicit concurrent_vector( size_type count, const allocator_type& alloc = allocator_type() );

构造一个包含count个就地默认构造元素的容器,调用grow_by(count)(concurrent_vector.h)。注意这里的语义:元素是就地(in-place)默认构造的,因此T必须满足可默认构造(default constructible)要求。

从迭代器区间复制

template <typename InputIterator> concurrent_vector( InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type() );

构造一个包含半开区间[first, last)中全部元素的concurrent_vector,调用grow_by(first, last)(concurrent_vector.h)。要求:InputIterator必须满足 ISO C++ 标准 [input.iterators] 一节对InputIterator的要求(只需单遍可读,不必是随机访问迭代器)。

从 initializer_list 构造

concurrent_vector( std::initializer_list<value_type> init, const allocator_type& alloc = allocator_type() );

等价于concurrent_vector(init.begin(), init.end(), alloc)。源码正是这样委托的(concurrent_vector.h),因此支持花括号初始化列表:

oneapi::tbb::concurrent_vector<int> v{1, 2, 3, 4, 5};

异常安全:try_call 机制

值得注意的源码细节是:上述所有"从序列构造"的构造函数在内部都使用了try_call(...).on_exception(...)包裹增长逻辑,一旦grow_by过程中任何元素构造抛出异常,便调用base_type::clear()回滚已构造的元素,保证构造失败时容器不会残留半初始化状态(concurrent_vector.h)。这是与std::vector类似的强异常安全保证,也是并发容器对生命周期一致性的重要承诺。

四、复制构造:深拷贝的语义与分配器选择

concurrent_vector( const concurrent_vector& other ); concurrent_vector( const concurrent_vector& other, const allocator_type& alloc );

复制构造:构造other的一个完整拷贝,逐一复制全部元素。

分配器获取规则:当未显式提供分配器时,通过std::allocator_traits<allocator_type>::select_on_container_copy_construction(other.get_allocator())获取(即"复制构造时分配器的选择")。源码确认了这一行为(concurrent_vector.h):

concurrent_vector( const concurrent_vector& other ) : base_type(segment_table_allocator_traits::select_on_container_copy_construction(other.get_allocator())) { try_call( [&] { grow_by(other.begin(), other.end()); } ).on_exception( [&] { base_type::clear(); }); }

若显式传入alloc,则直接以base_type(other, alloc)构造(concurrent_vector.h)。

并发约束:若复制构造与对other的其他操作并发进行,行为未定义(undefined behavior)。这是并发容器的通用规则——concurrent_vector的并发安全针对的是"增长 + 读"这类模式,而不是"结构性修改 + 结构性修改"。

五、移动构造:转移内部存储

concurrent_vector( concurrent_vector&& other ) noexcept; concurrent_vector( concurrent_vector&& other, const allocator_type& alloc );

移动构造:以移动语义取得other的内容。移动后other处于**有效但未指定(valid but unspecified)**的状态,即仍可被安全销毁或重新赋值,但其元素内容不再有保证。

  • 未指定分配器时,通过std::move(other.get_allocator())取得分配器;
  • 不带alloc参数的重载被标记为noexcept,这是因为底层segment_table的移动构造本身不抛异常(concurrent_vector.h);
  • 带alloc的重载用于"移动 + 换分配器"的场景,源码直接调用base_type(std::move(other), alloc)(concurrent_vector.h)。

并发约束:与other并发操作时行为未定义。

由于底层是分段表结构,移动构造通常只是转移段表指针与 size 计数,代价为 O(1);这一点与std::vector的移动构造类似,但 oneTBB 的段表扩展机制(allow_table_extending = true,见 concurrent_vector.h)意味着移动后源对象仍保留可复用的存储骨架。

六、析构函数:回收一切

~concurrent_vector();

析构concurrent_vector:逐一调用存储元素的析构函数,并归还所有使用的内存。源码中析构函数本体为空(~concurrent_vector() {},concurrent_vector.h),实际清理逻辑全部由基类segment_table的析构函数完成——包括遍历各段销毁元素、释放段表与元素存储。

并发约束:析构与*this上任何其他操作并发进行时行为未定义。这也意味着:当一个线程仍在遍历或访问容器时,另一个线程绝不能销毁它;在多线程程序中,容器的生命周期管理(谁创建、谁销毁、何时销毁)必须由外部同步机制(如引用计数、join 语义)保证。

七、赋值运算符:三种替换语义

拷贝赋值

concurrent_vector& operator=( const concurrent_vector& other );

用other中元素的拷贝替换*this中的全部元素,返回*this。若std::allocator_traits<allocator_type>::propagate_on_container_copy_assignment::value为true,则同时拷贝赋值分配器(否则保留原分配器,避免分配器状态冲突)。源码直接委托基类(concurrent_vector.h)。

并发约束:与*this或other并发操作时行为未定义。

移动赋值

concurrent_vector& operator=( concurrent_vector&& other ) noexcept(/*See below*/);

用移动语义将other的元素替换进*this,other置于有效但未指定状态。若propagate_on_container_move_assignment::value为true,则移动赋值分配器。

noexcept 规格(源码常量is_noexcept_assignment,concurrent_vector.h):

noexcept(std::allocator_traits<allocator_type>::propagate_on_container_move_assignment::value || std::allocator_traits<allocator_type>::is_always_equal::value)

直观理解:要么分配器随移动传播(新容器接管旧分配器),要么分配器总是相等(is_always_equal,如std::allocator<int>),移动赋值就保证不抛异常;反之若分配器不同且不传播,则可能触发重新分配而抛异常。这一 noexcept 条件对编写依赖"移动赋值不抛异常"的泛型代码(如std::vector的重分配路径)至关重要。

从 initializer_list 赋值

concurrent_vector& operator=( std::initializer_list<value_type> init );

用init中的元素替换*this的全部元素,等价于assign(init)(concurrent_vector.h),返回*this。与*this并发操作时行为未定义。

八、assign:容器内容的整体替换

void assign( size_type count, const value_type& value ); template <typename InputIterator> void assign( InputIterator first, InputIterator last ); void assign( std::initializer_list<value_type> init );

三个assign重载分别对应:count个value的副本、半开区间[first, last)的元素、以及 initializer_list(等价于assign(init.begin(), init.end()))。

从源码实现看,assign的执行策略是"先销毁、再增长"(concurrent_vector.h):

void assign( size_type count, const value_type& value ) { destroy_elements(); grow_by(count, value); }

即调用destroy_elements()销毁当前全部元素,再通过grow_by重新构造目标元素。因此assign的复杂度与元素数量成正比,且要求新元素类型可被构造;destroy_elements之后grow_by若抛异常,容器将处于"空但合法"的状态,仍可安全继续使用。

迭代器版assign通过std::enable_if<is_input_iterator<InputIterator>::value>参与重载决议(concurrent_vector.h),仅当InputIterator满足输入迭代器要求时才可用——这与文档中 [input.iterators] 的要求严格对应。

九、get_allocator:查询当前分配器

allocator_type get_allocator() const;

返回与*this关联的分配器的一份拷贝。源码实现为直接转发基类(concurrent_vector.h):

allocator_type get_allocator() const { return base_type::get_allocator(); }

该接口常用于:

  • 配合select_on_container_copy_construction语义,在自定义容器中复刻 oneTBB 的分配器传播规则;
  • 在复制/移动构造、reserve(concurrent_vector.h)等需要"用同款分配器分配新内存"的场景中获取分配器。

十、生命周期语义背后的并发增长机制

理解构造/赋值为何全部围绕grow_by展开,需要看到 oneTBB 的并发增长设计。concurrent_vector的"并发增长"接口(详见 concurrent_growth.rst)包括:

iterator grow_by( size_type delta ); iterator grow_by( size_type delta, const value_type& value ); template <typename ForwardIterator> iterator grow_by( ForwardIterator first, ForwardIterator last ); iterator grow_by( std::initializer_list<value_type> init ); iterator grow_to_at_least( size_type n ); iterator grow_to_at_least( size_type n, const value_type& value ); iterator push_back( const value_type& value ); iterator push_back( value_type&& value ); template <typename... Args> iterator emplace_back( Args&&... args );

这些接口(concurrent_vector.h)均通过internal_grow_by_delta/internal_emplace_back在分段表上分配新段并就地构造元素,多个线程可以同时调用而互不阻塞——这正是concurrent_vector区别于std::vector的核心价值:先并行构造(各线程各写一段),再顺序读取。

由此可推及生命周期的一条重要实践原则:

  1. 构造期:用构造函数或assign一次性建立初始内容(单线程阶段);
  2. 运行期:多线程通过grow_by/push_back并发追加,通过operator[]/at/迭代器并发读取(见 element_access.rst 与 iterators.rst),但不要在此时对同一容器执行赋值、析构等结构性修改;
  3. 收尾期:确保所有访问线程结束后,再由单一线程执行析构。

十一、源码与测试中的验证锚点

  • 容器类模板与全部生命周期接口:include/oneapi/tbb/concurrent_vector.h——构造函数(L282-344)、析构(L346)、赋值(L348-362)、assign(L364-379)、get_allocator(L479-481)。
  • noexcept 规格来源:concurrent_vector.h 中的is_noexcept_assignment。
  • 底层分段表结构:embedded_table_num_segments = 3(concurrent_vector.h)与长段表分配allocate_long_table(concurrent_vector.h)。
  • 并发增长接口文档:concurrent_growth.rst。
  • 迭代器实现:vector_iterator在跨段边界时会失效缓存指针、通过internal_subscript重新定位(concurrent_vector.h),这解释了为什么遍历期不允许容器被赋值/销毁。
  • 测试用例:test/tbb/test_concurrent_vector.cpp 中覆盖了grow_by各种重载(L199-224)、空区间grow_by不改变容器(L386-387)、迭代器区间增长(L391-397)、并发增长(L478、L622)以及移动迭代器增长(L107-117)等场景,可作为验证生命周期行为的实测参考。
  • 推导指南:自 C++17 起concurrent_vector还提供类模板实参推导(CTAD)指南,见 deduction_guides.rst,例如从迭代器区间直接推导元素类型。

十二、实战要点速览

  • 选择构造方式:需要count个相同值用(count, value)重载;需要默认构造元素用(count)重载;需要从已有序列复制用迭代器或 initializer_list 重载;其中迭代器重载要求输入迭代器,grow_by的迭代器版本则要求前向迭代器(因为需要std::distance求长度)。
  • 分配器纪律:复制构造时分配器经select_on_container_copy_construction派生;移动构造时经std::move(get_allocator())取得;赋值时是否传播分配器取决于propagate_on_container_copy/move_assignment特性。若需要跨分配器语义,应显式传入alloc参数。
  • noexcept 判断:移动构造无条件noexcept;移动赋值仅在"分配器传播或恒等"时为noexcept,泛型代码应依据is_noexcept_assignment静态常量(concurrent_vector.h)进行条件优化。
  • 并发红线:构造、析构、赋值、assign均不允许与同一对象的其他操作并发;只有"并发增长 + 并发读"是被支持的并发模式。任何跨线程共享容器,都必须在生命周期边界处做好同步。

通过将 construct_destroy_copy.rst 的接口规范与 concurrent_vector.h 的源码实现相互印证,开发者可以精确地把控concurrent_vector从创建到销毁的每一个环节,在多线程程序中安全、高效地管理共享数据的生命周期。

  • 并发编程
  • 高性能计算

【免费下载链接】oneTBB

oneAPI Threading Building Blocks (oneTBB)

项目地址:https://gitcode.com/gh_mirrors/on/oneTBB
点击查看免费下载
上一篇:如何用BetterNCM安装器3分钟打造你的专属网易云音乐
下一篇:BetterNCM安装器:让网易云音乐焕发新生的3分钟指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询