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

资讯详情

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

像写单线程一样写多线程:TaskScheduler子任务与Yield协程式风格实战

像写单线程一样写多线程:TaskScheduler子任务与Yield协程式风格实战 像写单线程一样写多线程TaskScheduler子任务与Yield协程式风格实战【免费下载链接】TaskSchedulerCross-platform, fiber-based, multi-threaded task scheduler designed for video games.项目地址: https://gitcode.com/gh_mirrors/tasksch/TaskSchedulerTaskScheduler 是一个面向游戏引擎的跨平台、基于纤程fiber的多线程任务调度器。它用「子任务 Yield 让出」的协程式风格让你像写单线程一样写多线程——逻辑自上而下线性书写真正的并行与等待全部交给调度器处理。本文带你快速掌握 TaskScheduler 子任务拆分与 Yield 协程风格的核心用法。为什么多线程这么难而 TaskScheduler 能救你传统 C 多线程写起来常常是这样回调地狱任务 A 等任务 B任务 B 又依赖 C代码被回调切得支离破碎忙等与轮询while (!finished) sleep(1)既费 CPU 又慢线程管理复杂要自己处理线程池、同步原语、数据竞争。TaskScheduler 的解法很巧妙把任务跑在**纤程fiber**上——纤程由用户态调度切换成本远低于操作系统线程。你的任务函数看起来是顺序执行的但可以在任意点Yield让出调度器随后把 CPU 给别的任务完成后再原地恢复执行。 一句话理解单线程的写法多线程的速度。这套设计源自顽皮狗Naughty Dog著名的 GDC 演讲 Parallelizing the Naughty Dog engine using fibers并已在商业游戏 SkyforgePC / PS4 / Xbox One中落地。核心概念速览3 个主角概念作用所在位置Task一个可调度任务通过MT_DECLARE_TASK宏声明实现Do(FiberContext)函数Scheduler/Include/MTTaskDesc.hFiberContext每个纤程的上下文任务通过它派生子任务、Yield 让出、查询状态Scheduler/Include/MTFiberContext.hTaskGroup任务组用于按组提交与批量等待Scheduler/Include/MTTaskGroup.h调度器入口是TaskScheduler类完整定义见Scheduler/Include/MTScheduler.h第 215 行起核心实现位于Scheduler/Source/MTScheduler.cpp。三步跑通第一个任务#include MTScheduler.h struct SimpleTask { MT_DECLARE_TASK(SimpleTask, MT::StackRequirements::STANDARD, MT::TaskPriority::NORMAL, MT::Color::Blue); void Do(MT::FiberContext) { // ... 这里写你的业务逻辑 ... } }; int main() { MT::TaskScheduler scheduler; SimpleTask tasks[1000]; // 1) 批量异步提交 scheduler.RunAsync(MT::TaskGroup::Default(), tasks, 1000); // 2) 等待全部完成等待期间当前线程也会帮忙执行未完成任务 scheduler.WaitAll(1000); return 0; }三个参数说明栈需求STANDARD/EXTENDED声明在Scheduler/Include/MTStackRequirements.h优先级TaskPriority::NORMAL等调度器据此分配工作颜色Color用于缓存亲和性优化把任务着色到合适的线程组。子任务实战父任务如何优雅地等子任务这是 TaskScheduler 最有价值的 APIcontext.RunSubtasksAndYield()声明于Scheduler/Include/MTFiberContext.h第 70 行模板实现在Scheduler/Include/MTFiberContext.inl第 27–41 行。它的行为分四步全程自动把子任务数组均匀分发到各 worker 线程的任务桶中按 worker 数量切分父任务立即Yield进入AWAITING_CHILD状态释放纤程所有子任务执行完毕后调度器自动恢复父任务恢复点之后的代码可以直接读取子任务写好的结果。struct ParentTask { MT_DECLARE_TASK(ParentTask, MT::StackRequirements::STANDARD, MT::TaskPriority::NORMAL, MT::Color::Blue); void Do(MT::FiberContext context) { SubTask subs[4]; // 1) 栈上创建子任务填好各自参数 context.RunSubtasksAndYield(MT::TaskGroup::Default(), subs, 4); // 2) 执行到这里时4 个子任务已全部完成 MergeResult(subs); // 3) 直接取结果像单线程一样线性 } };对比一下传统写法你不需要回调、不需要事件、不需要轮询——一行RunSubtasksAndYield就是一次并行 隐式等待。经典场景递归分治参考测试文件SchedulerTests/Tests/SubtasksTests.cpp中的DeepSubtaskQueue第 43–100 行任务 N 拆分出 N-1 与 N-2 两个子任务并RunSubtasksAndYield最终result taskNm1.result taskNm2.result。整棵任务树呈斐波那契式递归展开却写成了最朴素的递归函数——这正是「像写单线程一样写多线程」的最佳注脚。Yield 让出协程式风格的精髓除了等子任务你还能主动context.Yield()让出执行权声明见Scheduler/Include/MTFiberContext.h第 80 行void Do(MT::FiberContext context) { DoPhase1(); // 阶段一 context.Yield(); // 让出调度器去跑别的任务本任务原地冻结 DoPhase2(); // 稍后恢复接着执行 }Yield 的典型用途实现屏障barrier所有任务先跑完阶段一再统一进入阶段二。SchedulerTests/Tests/SimpleTests.cpp的YieldTasks测试第 147 行起就演示了这一点——每个任务在 phase 0 末尾context.Yield()恢复后确认本机所有任务的 phase 0 都已结束分帧削峰把耗时工作拆成若干小段每段之间 Yield避免单帧卡顿公平性控制长任务主动让出让短任务有机会插队执行。任务当前的状态可通过FiberTaskStatus枚举查看MTFiberContext.h第 43–53 行RUNNED、FINISHED、YIELDED、AWAITING_CHILD——分别对应已运行、已完成、已让出、等待子任务四种形态。⚠️实战提醒Yield 之后任务可能在另一个 worker 线程上恢复恢复后不要再使用 Yield 前缓存的线程相关状态应通过context.GetThreadContext()重新获取。RunAsync vs RunSubtasksAndYield怎么选维度scheduler.RunAsync(...)context.RunSubtasksAndYield(...)调用位置普通线程如主线程任务内部worker 线程中是否等待否提交后立即返回是父任务挂起直到子任务全部完成父子关系无自动建立父子依赖支持递归分治典型场景主循环批量提交帧任务任务内部拆分子任务并行加速两者都在Scheduler/Include/MTFiberContext.inl中实现RunAsync见第 44–60 行。经验法则顶层提交用scheduler.RunAsync任务内拆分用context.RunSubtasksAndYield。新手常见坑与最佳实践context.RunAsync不能在任务外调用——它断言必须在 worker 线程中使用普通线程请用scheduler.RunAsync单次提交的子任务数量有上限——RunSubtasksAndYield要求taskCount TASK_BUFFER_CAPACITY - 1见MTFiberContext.inl第 29 行大批量任务请分批提交栈上子任务的生存期——RunSubtasksAndYield是同步等待语义函数返回时子任务必然已完成因此栈上数组是安全的但RunAsync提交后函数不能立刻返回子任务对象必须活得比提交更久可参照SchedulerTests/Tests/FireAndForget.cpp的句柄用法Yield 后重取线程状态——如上所述恢复点可能换了线程按组管理用scheduler.CreateGroup()WaitGroup()可以只等某一批任务如只等动画更新组完成再进入下一帧阶段示例见SchedulerTests/Tests/WaitTests.cpp。项目结构导航去哪里读源码调度器顶层 APIScheduler/Include/MTScheduler.h与Scheduler/Source/MTScheduler.cpp纤程上下文子任务 / Yield 核心Scheduler/Include/MTFiberContext.h、Scheduler/Include/MTFiberContext.inl、Scheduler/Source/MTFiberContext.cpp任务描述 / 任务组 / 栈需求Scheduler/Include/MTTaskDesc.h、Scheduler/Include/MTTaskGroup.h、Scheduler/Include/MTStackRequirements.h平台层纤程实现纤程跳转基于 Boost.ContextScheduler/Include/Platform/Windows/MTFiber.h、ThirdParty/Boost.Context/fcontext.h上手示例强烈推荐通读SchedulerTests/Tests/SubtasksTests.cpp、SchedulerTests/Tests/SimpleTests.cpp、SchedulerTests/Tests/GroupTests.cpp构建配置根目录premake4.lua更多用法示例可看SchedulerTests/Tests/下全部测试总结TaskScheduler 用最少的 API 换来了最直观的多线程写法拆分MT_DECLARE_TASK声明任务RunAsync批量提交并行等待context.RunSubtasksAndYield一行完成分发 挂起 恢复协程风格context.Yield主动让出实现屏障、分帧与公平调度。掌握这三招你就能像写单线程一样写出跑满多核的游戏代码。建议从SchedulerTests/Tests/SubtasksTests.cpp开始动手实验亲自感受任务树递归展开的优雅。【免费下载链接】TaskSchedulerCross-platform, fiber-based, multi-threaded task scheduler designed for video games.项目地址: https://gitcode.com/gh_mirrors/tasksch/TaskScheduler创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表