尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TBB aggregator_ext 专家接口实战:面向数据聚合的互斥操作调度与 handler 自定义

TBB aggregator_ext 专家接口实战:面向数据聚合的互斥操作调度与 handler 自定义 TBB aggregator_ext 专家接口实战面向数据聚合的互斥操作调度与 handler 自定义【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold本篇技术指南围绕 oneAPI Threading Building BlocksoneTBB中aggregator_ext类aggregator 专家接口展开它允许开发者把任意继承自aggregator_operation的数据对象提交给聚合器并以自定义函数对象handler批量、互斥地处理这些操作是替代普通 mutex、降低锁竞争开销的高阶手段。文章结合当前仓库 expert_interface.rst 官方规范与 detail/_aggregator.h 底层实现给出可直接编译运行的 priority_queue 示例并剖析pending_operations原子链表与handler_busy标志位的工作原理。读完你将掌握aggregator_ext的完整成员语义、handler 编写规范以及它在concurrent_priority_queue等容器中的真实工程用法。一、聚合器aggregator的定位为什么需要它oneTBB 的 aggregator 类是一种不建模 Mutex Concept 的互斥机制。与普通互斥锁最大的区别在于mutex 一次只允许一个线程进入临界区而 aggregator 会把来自多个线程的操作收集成链表再由单个线程串行批量处理从而把多次锁获取/释放的开销摊薄为一次批量调度。官方规范 aggregator_cls.rst 将其定位为用于互斥执行的类并分为两个层级接口接口层级文档核心方法适用人群基础接口basic_interface.rstexecute(body)传入函数体/lambda常规场景专家接口expert_interface.rstprocess(op)传入数据对象 自定义 handler需要精细控制操作数据与处理逻辑的高级用户基础接口的用法非常直观my_aggregator.execute([]{ ... })即可把一段 lambda 提交给聚合器互斥执行。而专家接口aggregator_ext放弃了传函数的方式改为传数据调用方构造一个携带操作数据的对象派生自aggregator_operation通过process提交聚合器内部将若干等待中的操作串成链表交给用户自定义的 handler 函数对象统一处理。这样做的收益是操作数据可以在多个处理节点间复用、handler 可以针对一批操作做合并优化例如批量分配、批量更新这正是concurrent_priority_queue内部使用的模式。二、声明、头文件与预览宏aggregator_ext的模板声明与包含方式如下templatetypename handler_type class aggregator_ext;使用前必须定义预览宏并包含对应头文件#define TBB_PREVIEW_AGGREGATOR 1 #include oneapi/tbb/aggregator.hTBB_PREVIEW_AGGREGATOR表明该接口当前处于 oneTBB 预览preview阶段接口签名在后续版本中可能调整。当前仓库携带的 TBB 版本为 2023.0见 include/oneapi/tbb/version.h 中的TBB_VERSION_MAJOR 2023与TBB_VERSION_MINOR 0。三、成员全景aggregator_operation与aggregator_ext官方文档给出的完整成员声明如下namespace oneapi { namespace tbb { class aggregator_operation { public: enum aggregator_operation_status {agg_waiting0, agg_finished}; aggregator_operation(); void start(); void finish(); aggregator_operation* next(); void set_next(aggregator_operation* n); }; templatetypename handler_type class aggregator_ext { public: aggregator_ext(const handler_type h); void process(aggregator_operation *op); }; } // namespace tbb } // namespace oneapi各成员的语义逐条说明如下成员说明aggregator_ext(const handler_type h)构造aggregator_ext对象并用 handlerh处理所有被提交的操作。void process(aggregator_operation* op)将op携带的操作数据提交给聚合器以互斥方式执行当op被 handler 处理完毕finish被调用后返回。aggregator_operation::aggregator_operation()构造一个基础aggregator_operation对象初始状态为agg_waiting。void aggregator_operation::start()准备该操作对象以被处理。handler 在访问节点携带的数据之前必须调用它。void aggregator_operation::finish()准备将该操作对象释放回其发起线程。调用后发起线程在process中的等待随即解除。aggregator_operation* aggregator_operation::next()返回链表中紧随this的下一个操作节点。void aggregator_operation::set_next(aggregator_operation* n)将n设为this的下一个节点。状态枚举只有两个值agg_waiting 0操作尚未被处理与agg_finished操作已被处理完毕。process内部正是通过轮询该状态字段来判断是否可以把控制权交还给发起线程。四、底层原理原子等待链表与 handler 互斥标志专家接口并非凭空设计其核心机制在 detail/_aggregator.h 中有着清晰对应。文档中的aggregator_operation对应源码中的aggregated_operationDerived模板基类// Base class for aggregated operation template typename Derived class aggregated_operation { public: // Zero value means wait status, all other values are user specified values std::atomicuintptr_t status; std::atomicDerived* next; aggregated_operation() : status{}, next(nullptr) {} };可以看到两个关键字段status原子的状态字段。0表示等待中其余非零值由使用者自定义文档示例中用success等业务字段区分实际调度仍以 status 是否归零判断完成next原子的链表指针多个被提交的操作由此串成等待链表这也是next()/set_next()的底层存储。聚合器本体在源码中对应aggregator_generictemplate typename OperationType class aggregator_generic { public: aggregator_generic() : pending_operations(nullptr), handler_busy(false) {} template typename HandlerType void execute( OperationType* op, HandlerType handle_operations, bool long_life_time true ); private: void start_handle_operations( HandlerType handle_operations ); std::atomicOperationType* pending_operations; // 待处理操作的原子链表 std::atomicuintptr_t handler_busy; // 控制 handler 的访问 };从源码结构可以还原出完整的调度流程入队调用方在execute中先把op-next原子地指向当前链表头再通过compare_exchange_strong把op插入pending_operations链表。此时使用memory_order_relaxed即可因为同步语义由后续的状态等待保证。抢占 handler如果pending_operations原值为空即res nullptr本线程是第一个入队者则调用start_handle_operations先自旋等待handler_busy归零然后置 1 独占 handler取出整条链表交给handle_operations(op_list)处理处理完毕将handler_busy以memory_order_release清 0。等待完成若非第一个入队者则自旋等待op-status从 0 变为非 0spin_wait_while_eq直到 handler 对该节点调用finish即写入非零状态后返回。值得注意的细节execute中还有long_life_time参数——为false时表示操作对象可能在处理期间被销毁短生命周期此时等待该操作完成属于未定义行为文档示例中的操作对象是栈上变量且全程存续属于long_life_time语义可以安全地等待process返回。从源码看process的处理完毕才返回正是通过这一等待循环实现的。五、实战示例用aggregator_ext保护非并发std::priority_queue官方文档给出完整示例用aggregator_ext安全地操作一个非并发容器std::priority_queue。下面按文档原意整理为可直接编译的形式。首先定义操作数据类型——文档中以aggregator_node命名基类对应当前仓库 detail/_aggregator.h 中的aggregated_operation#include queue #include vector #include functional #include oneapi/tbb/aggregator.h typedef priority_queuevalue_type, vectorvalue_type, compare_type pq_t; pq_t my_pq; value_type elem 42; // 操作数据派生自聚合操作节点 class op_data : public aggregated_operationop_data { public: value_type* elem; bool success, is_push; op_data(value_type* e, bool pushfalse) : elem(e), success(false), is_push(push) {} };然后是核心的 handler 函数对象。handler 的签名必须符合规范接收一个聚合操作节点的链表遍历并处理其中所有节点全部处理完毕后才能返回class my_handler_t { pq_t *pq; public: my_handler_t() {} my_handler_t(pq_t *pq_) : pq(pq_) {} void operator()(aggregated_operationop_data* op_list) { op_data* tmp; while (op_list) { tmp static_castop_data*(op_list); op_list op_list-next(); // 先取下一个节点 tmp-start(); // 处理前必须 start if (tmp-is_push) { pq-push(*(tmp-elem)); } else { if (!pq-empty()) { tmp-success true; *(tmp-elem) pq-top(); pq-pop(); } } tmp-finish(); // 处理完毕必须 finish } } };最后创建聚合器并提交操作// 创建 aggregator_ext 并以 handler 实例初始化 aggregator_extmy_handler_t my_aggregator(my_handler_t(my_pq)); // 向优先队列 push 一个元素 op_data my_push_op(elem, true); my_aggregator.process(my_push_op); // 从优先队列 pop 一个元素 bool result; op_data my_pop_op(elem); my_aggregator.process(my_pop_op); result my_pop_op.success;这个示例体现了专家接口的完整使用纪律官方文档特别强调了三件事handler 必须处理链表中的全部节点。遍历顺序由用户自行决定但 handler 返回前所有节点都必须被处理否则对应线程会在process中永久自旋等待。对链表的操作只能通过next()/set_next()进行。任何对节点指针的裸操作都可能破坏聚合器的原子链表不变量。每个节点处理前必须调用start()完成后必须调用finish()。start标志该节点已被接管防止与发起线程的数据竞争finish则将节点释放回发起线程使其在process中的等待立即返回。六、从文档到工程TBB 内部容器如何复用聚合器专家接口并非孤立设计oneTBB 自己的并发容器就是聚合器机制的真实消费者。最典型的例子是 concurrent_priority_queue.h第 230 行操作类型cpq_operation直接继承自aggregated_operationcpq_operation携带typePUSH_OP / POP_OP / PUSH_RVALUE_OP与elem/sz联合体第 241-251 行functor内部持有容器指针operator()(cpq_operation* op_list)转发到handle_operations第 173-203 行push、try_pop等公有接口各自构造cpq_operation并调用my_aggregator.execute(op_data)随后检查op_data.statusSUCCEEDED/FAILED判断是否抛出bad_alloc。其handle_operations实现体现了批量聚合的真正威力第 253 行起第一遍遍历把所有 push/pop 操作分组处理——pop 操作被暂存到本地pop_listpush 先落盘后续再对pop_list统一执行弹出从而把多次互斥区间合并为一次显著减少原子操作与缓存行颠簸。另一个使用场景是 concurrent_lru_cache.h它定义了retrieve_aggregator_operation与signal_end_of_usage_aggregator_operation两种派生操作通过同一个聚合器串行执行缓存查找与使用计数更新并用aggregating_functor桥接到handle_operations。这印证了专家接口的核心价值多类操作共享同一互斥通道由 handler 依据节点类型分派处理。七、基础接口与专家接口的选型建议对照同目录下的 basic_interface.rst基础接口aggregatorexecute(lambda)适合操作逻辑简单、无需复用数据对象的场景。它内部同样走_aggregator.h的等待链表只是由库替你包装了操作节点与 handler代码量最小。专家接口aggregator_extprocess(data) 自定义 handler适合需要把操作数据与处理逻辑解耦的场景——例如需要批量合并、按类型分派、或操作对象需要在多个阶段间传递数据时。无论是哪种接口只要提交到同一个聚合器对象所有操作都保证互斥执行而聚合器相较普通 mutex 的额外收益是批量摊薄调度开销。如果你只是想在多个线程间保护一段短临界区优先考虑spin_mutex等轻量锁当临界区操作可以合并、且对吞吐有更高要求时再升级到 aggregator 家族。八、小结aggregator_ext专家接口把互斥调度的控制权完整交给开发者以aggregator_operation承载操作数据以自定义 handler 批量消费等待链表以start/finish精确控制节点生命周期。本文从 expert_interface.rst 规范出发结合 detail/_aggregator.h 的原子实现与 concurrent_priority_queue.h 的真实工程案例完整覆盖了该接口的声明、成员语义、示例代码与底层原理。需要进一步阅读时可对比 basic_interface.rst 了解基础接口或直接阅读 concurrent_priority_queue.h 中handle_operations的批量分派实现体会聚合器在真实并发容器中的设计精髓。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表