ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

oneTBB 流图在非主线程中运行时的安全销毁:用 task_arena::enqueue 等待并销毁图的正确姿势

oneTBB 流图在非主线程中运行时的安全销毁:用 task_arena::enqueue 等待并销毁图的正确姿势 并发编程高性能计算【免费下载链接】oneTBBoneAPI Threading Building Blocks (oneTBB)项目地址https://gitcode.com/gh_mirrors/on/oneTBB点击查看免费下载本指南围绕 oneAPI Threading Building BlocksoneTBB流图flow graph生命周期管理中的一个高频陷阱展开当图的构建与执行发生在主线程之外时如何在不阻塞主线程的前提下安全等待并销毁图。读完本文你将掌握graph::wait_for_all的调用时机、task_arena::enqueue的异步任务封装模式以及如何借助信号机制确认后台图的完成状态从而彻底规避图被提前销毁导致程序崩溃这类隐性故障。问题背景销毁图之前为什么必须 wait_for_alloneTBB 官方用户指南中的 Always Use wait_for_all() 一节明确指出流图编程中最常见的错误之一就是忘记调用wait_for_all。graph::wait_for_all会阻塞当前线程直到该图派生的所有任务全部完成。这不仅用于等待计算结束更是销毁图或其任何节点前的必要步骤。下面这段代码会在程序运行中直接失败void no_wait_for_all() { graph g; function_node int, int f( g, 1, []( int i ) - int { return spin_for(i); } ); f.try_put(1); // program will fail when f and g are destroyed at the // end of the scope, since the body of f is not complete }失败机理当函数作用域结束g和节点f被销毁。但此时执行f函数体的任务仍然在飞行中in flight。任务完成后会回头查找它所在节点的后继successor而此时图和节点都已被删除形成典型的悬空访问use-after-free。在函数末尾补上g.wait_for_all()即可防止图和节点的过早销毁。官方指南给出的排错建议同样直白如果使用流图时看到诡异行为先检查是否调用了wait_for_all。从源码看 graph 析构与 wait_for_all 的关系oneTBB 在实现层已经为忘记调用 wait_for_all设置了兜底防线。在 include/oneapi/tbb/flow_graph.h 中graph的析构函数本身就调用了wait_for_all()inline graph::~graph() { wait_for_all(); if (own_context) { my_context-~task_group_context(); r1::cache_aligned_deallocate(my_context); } delete my_task_arena; }这意味着图在析构时会自动等待其所有任务完成析构过程本身是安全的。而always_use_wait_for_all.rst中那个会导致程序失败的例子其崩溃的真正来源在于节点f作为graph_node其析构函数会调用my_graph.remove_node(this)见 flow_graph.h——当图的析构先于节点销毁发生时节点销毁时引用的图对象状态已经不可用从而触发未定义行为。由此可以提炼出两条铁律销毁顺序必须先销毁节点、再销毁图或保证图中所有任务已完成后再销毁节点显式等待不要依赖析构函数中的隐式wait_for_all作为唯一保障尤其是当图与节点生命周期交织、且任务可能仍在执行时显式调用wait_for_all是官方强烈建议的做法。核心场景图在主线程之外运行你并非总希望用wait_for_all()阻塞主应用线程。但是在销毁图之前对它调用wait_for_all是最安全的做法。当图在后台线程中构建、运行而主线程不希望原地干等时一个常见且优雅的解决方案是把构建图 等待图完成整体封装成一个任务通过task_arena::enqueue投递到任务竞技场中执行。推荐模式enqueue 一个后台任务destroy_graphs_outside_main_thread指南给出的完整示例原文代码直接可编译运行class background_task { public: void operator()() { graph g; function_node int, int f( g, 1, []( int i ) - int { return spin_for(i); } ); f.try_put(1); g.wait_for_all(); } }; void no_wait_for_all_enqueue() { task_arena a; a.enqueue(background_task()); // do other things without waiting… }这个模式的关键点在于图的构建、投递、等待全部在background_task::operator()内完成g.wait_for_all()保证图在函数体结束前完成随后的graph与节点析构也就绝对安全task_arena a与a.enqueue(...)组合让任务在 oneTBB 的任务竞技场中异步执行调用线程通常是主线程立即返回可以继续做其他事情竞技场对象a的默认构造即绑定默认竞技场enqueue会在竞技场中产生一个执行任务而不要求调用线程加入该竞技场。enqueue 的底层实现task_arena::enqueue的语义在 include/oneapi/tbb/task_arena.h 中有明确注释将处理函数对象的任务入队到竞技场并立即返回不需要调用线程加入竞技场。其实现路径为templatetypename F void enqueue(F f) { initialize(); enqueue_impl(std::forwardF(f), this); }enqueue_impl见 task_arena.h会将函数对象包装进enqueue_task继承自 oneTBB 内部task类型随后通过r1::enqueue投递到调度器。任务的execute方法执行用户函数体后自我回收m_allocator.delete_object异常则触发断言。这套实现保证了enqueue 是即发即忘fire-and-forget式的异步投递函数对象会在某个 worker 线程上于不确定的时刻执行。重要边界enqueued 任务的执行时机与完成信号指南在示例之后专门强调了这条边界这也是最容易出错的地方In the code snippet above, the enqueued task executes at some point, but its not clear when. If you need to use the results of the enqueued task, or even ensure that it completes before the program ends, you will need to use some mechanism to signal from the enqueued task that the graph is complete.即enqueued 任务何时执行是不确定的。若你需要使用该任务的产出结果或需要保证它在程序结束前完成就必须引入一种从任务内部发出的图已完成信号机制。常见做法包括std::promise/std::future在background_task构造时传入std::promise函数体末尾set_value主线程用future.wait()等待主线程获取结果如function_node经try_put后写入的共享容器前必须确认信号已就绪原子标志 条件变量任务完成时置位原子标志并唤醒等待线程task_group与task_arena::wait_foroneTBB 的task_group支持跨线程等待语义——task_arena还提供了wait_for(task_group)接口task_arena.h可让主线程在不阻塞竞技场调度的前提下等待任务组完成task_arena::execute替代方案如果后台图必须与主线程有确定的同步点execute(F)task_arena.h会让调用线程加入竞技场执行函数并等待其返回语义上接近同步执行 等待完成。代码示例用 future 确认后台图完成#include future void no_wait_for_all_enqueue_with_signal() { task_arena a; std::promisevoid done; a.enqueue([done] { graph g; function_node int, int f( g, 1, []( int i ) - int { return spin_for(i); } ); f.try_put(1); g.wait_for_all(); // 图内任务全部完成 done.set_value(); // 向主线程发信号 }); // 主线程继续做其他事情…… done.get_future().wait(); // 需要结果时再同步等待 // 此时后台图已安全销毁可安全访问其结果 }注意done.set_value()必须放在g.wait_for_all()之后保证信号发出时图内所有任务确已完成而非仅图对象仍存活。相关实践与进阶阅读本文主题属于 oneTBB 官方等待与销毁流图系列技巧同系列还包括Always Use wait_for_all()wait_for_all的必要性分析与经典反例Avoid Dynamic Node Removal运行中动态删除节点同样是图生命周期的高危操作Flow Graph Tips for Waiting for and Destroying a Flow Graph上述主题的汇总入口。验证建议仓库测试集如 test/tbb/test_eh_flow_graph.cpp、test/conformance/conformance_graph.cpp覆盖了图的构建、等待、取消、异常传播等生命周期场景可在此基础上自行构造后台图 信号用例用task_arena::enqueue模式验证本文结论。若在调试中遇到莫名崩溃或数据错乱请按官方建议优先检查是否在销毁前调用了wait_for_all。赞分享并发编程高性能计算【免费下载链接】oneTBBoneAPI Threading Building Blocks (oneTBB)项目地址https://gitcode.com/gh_mirrors/on/oneTBB点击查看免费下载相关推荐Radix Vue FocusScope 组件深度解析聚焦范围、焦点陷阱与自动聚焦控制Radix Vue FocusScope 组件深度解析聚焦范围、焦点陷阱与自动聚焦控制 本文以 Radix Vueradix vue 仓库的 FocusS并发编程高性能计算ARC运行器生命周期创建、运行、销毁全过程ARC运行器生命周期创建、运行、销毁全过程 GitHub Actions Runner ControllerARC是一个强大的Kubernetes Ope后端云原生容器编排CI/CD弹性伸缩DXVK Vulkan实例销毁时机安全实践DXVK Vulkan实例销毁时机安全实践 1. 引言为什么销毁时机至关重要 在基于Vulkan的Direct3D实现中Vulkan实例Instance图形学游戏开发上一篇Path of Building PoE2完整指南3步掌握流放之路2最强角色规划下一篇AMD MiniMax-M2.1-MXFP4 API使用指南从基础调用到高级配置创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表