十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter Impeller Vulkan 后端线程模型详解:并发工作池、栅栏等待器与资源管理器

Flutter Impeller Vulkan 后端线程模型详解:并发工作池、栅栏等待器与资源管理器 Flutter Impeller Vulkan 后端线程模型详解并发工作池、栅栏等待器与资源管理器【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutterFlutter 引擎的 Impeller 渲染器 Vulkan 后端并非单线程 GPU的简单模型而是围绕帧关键路径精心拆分为三类专用线程随上下文创建的并发工作池1–4 个 worker、独立运行的 Fence Waiter栅栏等待器与独立的 Resource Manager资源管理器。读完本文你将理解这套线程分工的设计动机为什么长任务不能进工作池、为什么回收资源要单独开线程、各线程的实际数量公式并能结合仓库中的 fence_waiter_vk.cc、resource_manager_vk.cc 与 context_vk.cc 源码完整还原一次 GPU 命令提交后的线程交互全过程。一、总览三类线程与线程总数公式Vulkan 后端的线程体系由三部分组成它们的生命周期全部绑定在ContextVK上并发工作池Concurrent Worker Pool在 Vulkan 上下文创建时一并创建用于并行化帧内 CPU 工作如 PSO 构建、帧负载分发。Fence Waiter栅栏等待器运行在自己的独立线程上负责等待 GPU 栅栏fence完成信号其核心职责是保证资源的引用计数至少与访问该资源的 GPU 命令缓冲区存活同样久。Resource Manager资源管理器运行在另一条独立线程上负责资源的回收collection与池化pooling。之所以单独开线程是因为触碰分配器是一个潜在的高开销操作——如果把回收工作放在帧负载中执行或放在 fence waiter 线程上执行都可能造成掉帧卡顿jank。由此可以得到线程总数的计算公式原文档明确给出的结论Impeller Vulkan 后端使用的线程总数 并发工作池中的 worker 数量 2Fence Waiter 线程 Resource Manager 线程这三类组件在 context_vk.h 中作为ContextVK的成员被持有fence_waiter_、resource_manager_、raster_message_loop_即并发工作池的载体并通过GetFenceWaiter()、GetResourceManager()、GetConcurrentWorkerTaskRunner()对外暴露。二、并发工作池数量如何计算以及禁止长任务红线2.1 工作池随上下文创建ContextVK::Setup()中的这段代码展示了工作池的创建时机——它在上下文的 Setup 阶段通过fml::ConcurrentMessageLoop一次性建好见 context_vk.ccvoid ContextVK::Setup(Settings settings) { TRACE_EVENT0(impeller, ContextVK::Setup); // ... raster_message_loop_ fml::ConcurrentMessageLoop::Create( ChooseThreadCountForWorkers(std::thread::hardware_concurrency())); // ... }2.2 Worker 数量的取值范围Worker 数量并非等于 CPU 核数而是由一个显式钳制函数决定context_vk.cc// static size_t ContextVK::ChooseThreadCountForWorkers(size_t hardware_concurrency) { // Never create more than 4 worker threads. Attempt to use up to // half of the available concurrency. return std::clamp(hardware_concurrency / 2ull, /*lo*/1ull, /*hi*/4ull); }取值规则可以归纳为一张表CPU 并发度计算concurrency / 2最终 Worker 数线程总数含 2 条专用线程211342248446126 → 钳制4上限6168 → 钳制4上限6即 worker 数恒为clamp(核数/2, 1, 4)整个 Vulkan 后端的 CPU 线程数最多为 6。该函数被标注 Visible for testing说明其为可测试而公开。2.3 为什么长任务不能投递到这个池这是原文档最强调的一条设计约束与 IO 工作池等其他池不同本池不允许投递长时间运行的任务。原因是帧工作负载frame workloads会被分发到这些 worker 上并行执行如果一个潜在耗时很久的任务文档给出的典型例子是纹理解压缩恰好占用某个 worker就可能阻塞帧关键任务直接造成掉帧。文档同时说明了这一独立池设计背后的另一层限制当前无法为具体任务指定 QoS服务质量/优先级因此只能用物理隔离线程池来 workaround 这个限制——把帧关键任务圈定在专属池内。文档明确预期随着任务级 QoS 能力到位这一限制未来可能被解除。从源码结构看帧内任务正是通过该池的 task runner 分发的ContextVK::GetConcurrentWorkerTaskRunner()直接返回raster_message_loop_-GetTaskRunner()context_vk.cc各渲染组件用它来并行化 PSO 等构建工作对应下文时序图中的 Setup PSO 步骤。三、Fence Waiter保证资源引用计数活得比 GPU 命令更久3.1 职责与线程特性Fence Waiter 的存在是为了维持一个关键不变量资源的引用计数生命周期必须至少与访问该资源的 GPU 命令缓冲区一样长——即 GPU 还在使用某张纹理/缓冲时CPU 侧绝不能把它析构掉。实现位于 fence_waiter_vk.h 与 fence_waiter_vk.cc其线程细节值得逐条对照线程命名与绑核线程启动时自命名为IplrVkFenceWait并通过fml::RequestAffinity(fml::CpuAffinity::kEfficiency)绑定到效率核——源码注释解释了原因这条线程大部分时间在等栅栏不需要快fence_waiter_vk.cc。等待集合WaitSet内部用WaitSetstd::vectorstd::shared_ptrWaitSetEntry维护待等待栅栏每个WaitSetEntry持有一个vk::UniqueFence和一个完成后回调fml::ScopedCleanupClosure。条件变量驱动的休眠/唤醒主循环在没有栅栏时挂起在条件变量上AddFence()在锁内先同步执行submit_callback提交 GPU 工作成功后把条目压入等待集合并notify_one()fence_waiter_vk.cc。带超时的批量等待Wait()对当前未 signaled 的栅栏集合调用device.waitForFences(..., waitAllfalse, timeout100ms)。任何一个栅栏先完成即可返回若集合中唯一的栅栏长时间不完成100ms 超时会使其退出本轮等待。若返回了非 Success/Timeout 的结果则打印验证日志并拆除等待线程保证出错时不会永久挂死。解锁后再析构被 signaled 的条目先在锁外拷出再调用其析构析构会触发完成回调、可能触碰分配器——这一Make sure the mutex is unlocked before calling the destructors的注释fence_waiter_vk.cc正是回收工作不要卡在热路径上原则的又一次体现。优雅停机Terminate()置位terminate_并notify_one()随后WaitUntilEmpty()排空剩余栅栏再join()线程析构函数会隐式调用Terminate()。3.2 提交路径CommandQueueVK 如何接入Fence Waiter 并非凭空运转——每次提交命令缓冲区时都会挂上一个栅栏。CommandQueueVK的提交逻辑command_queue_vk.cc// Submit will proceed, call callback with true when it is done and do not // call when reset is collected. auto fence_status context-GetFenceWaiter()-AddFence( std::move(fence), submit_callback, std::move(fence_complete_callback)); if (!fence_status.ok()) { tracker-RecordCompletion(submission_id); return fence_status; } reset.Release();其中submit_callback负责真正调用 Vulkan 的队列提交fence_complete_callback则在线程完成回调中把命令缓冲区的reset资源释放掉——这正是引用计数活得比 GPU 命令久这一不变量的落地方式reset内部以UniqueResourceVKT形式持有资源只有在栅栏 signaled 之后才被释放。四、Resource Manager把触碰分配器移出炉内热路径4.1 设计动机回收 Vulkan 资源意味着调用vkDestroyBuffer、vkDestroyImage等分配器接口属于潜在昂贵操作。原文档给出的结论是回收若发生在帧负载或 fence waiter 线程上都可能造成卡顿因此单独开一条 Resource Manager 线程批量执行。4.2 核心 API 与线程实现resource_manager_vk.h 定义了三层结构ResourceVK可被回收资源的基类标记接口ResourceVKTResourceType_把任意 move-constructible 资源包一层供管理器统一持有UniqueResourceVKTResourceType_面向使用者的唯一句柄构造时绑定一个weak_ptrResourceManagerVK析构或调用Reset()/Swap()时会把旧资源Reclaim给管理器。其operator-()中有一句防御性断言如果这里段错误用更友好的报错替代——直接访问已回收句柄会触发FML_CHECK而非难查的野指针崩溃。线程实现resource_manager_vk.cc与 Fence Waiter 同构线程自命名IplrVkResMgr同样请求效率核亲和性注释说明这条线程会调用析构函数但不需要特别快只要别打断 raster 线程主循环等待在条件变量上条件为有可回收资源或应退出有资源时先在锁内把整批Reclaimables换出swap然后解锁再在TRACE_EVENT0(Impeller, ReclaimResources)跟踪事件内清空该批次即调用各资源析构——再次印证持锁不碰分配器的纪律Reclaim()本身只做加锁入队 notify对调用方是低成本的构造函数中有一条历史教训注释线程必须在线程对象作为成员创建而不是在静态工厂里建否则会出现析构函数永不执行、线程永不终止的问题并引用了仓库中 issue 134482 作为佐证。谁在用它从源码中可以看到UniqueResourceVKT的实际使用者纹理资源 allocator_vk.ccUniqueResourceVKTImageResource、设备缓冲 device_buffer_vk.hUniqueResourceVKTBufferResource、后台命令池 command_pool_vk.ccUniqueResourceVKTBackgroundCommandPoolVK。即纹理、缓冲、命令池这类会频繁重建的资源释放时都只是入队真正的销毁延迟到 Resource Manager 线程的某个非帧关键时机。测试方面command_pool_vk_unittests.cc 用UniqueResourceVKTDeathRattle验证了资源离开作用域后确实被异步回收回调触发waiter.Signal()而 fence_waiter_vk_unittests.cc 则专门覆盖 Fence Waiter 的行为。五、线程交互时序从启动到一帧的完整生命周期原文档给出了一张 Mermaid 时序图概括了各线程在一次应用启动与单帧渲染中的协作关系完整保留如下对照源码可以逐条落实这张图启动阶段Application launchRender Thread 把 PSO 构建等并行任务分发到 Concurrent Worker 1/2对应经由GetConcurrentWorkerTaskRunner()投递到ConcurrentMessageLoop的任务worker 完成后回传 Done。每帧循环One FrameRender Thread 把帧负载继续分发给 worker帧内并行被 GPU 占用的资源Resource 1/2 owned by GPU通过AddFence路径登记到 Fence Waiter由提交回调连同栅栏一起注册Render Thread 直接向 GPU 队列提交命令。GPU 侧完成后Fence Waiter 在waitForFences中被唤醒GPU Work Done随后触发完成回调释放资源引用被释放的资源句柄经Reclaim进入 Resource Manager 的队列Collect/Pool Resources由其线程在锁外批量销毁。这条链路的要点是责任链单向流转Render Thread 只做提交Fence Waiter 只做等待与引用释放Resource Manager 只做销毁——昂贵操作被逐层推离帧关键路径。六、生命周期与停机顺序线程的创建与销毁顺序同样有讲究。ContextVK::Shutdown()context_vk.cc给出了确定的拆除序列void ContextVK::Shutdown() { // There are multiple objects, for example |CommandPoolVK|, that in their // destructors make a strong reference to |ContextVK|. Resetting these shared // pointers ensures that cleanup happens in a correct order. // // tl;dr: Without it, we get thread::join failures on shutdown. fence_waiter_-Terminate(); resource_manager_.reset(); raster_message_loop_-Terminate(); }注释直白地点出了原因CommandPoolVK等对象析构时会强引用ContextVK不按序释放会导致thread::join失败——因此先终止 Fence Waiter再释放 Resource Manager最后终止工作池。ResourceManagerVK的析构函数中还有一条针对测试的断言如果 ResourceManager 是在它自己派生的线程上被析构说明ContextVK没有被正确 shutdown常见修法是在测试结束时先调用context-Shutdown()resource_manager_vk.cc——这对集成 Impeller 的宿主方嵌入器、测试夹具是一个明确的接入契约。七、小结组件线程数运行位置/绑核职责对应源码并发工作池clamp(核数/2, 1, 4)普通核并行化 PSO 构建、帧负载禁止长任务context_vk.ccFence Waiter1效率核IplrVkFenceWait等待栅栏、保证资源引用计数 ≥ GPU 命令存活期fence_waiter_vk.ccResource Manager1效率核IplrVkResMgr批量回收/池化资源隔离分配器开销resource_manager_vk.ccVulkan 后端的线程模型本质上是一套**按代价分配线程**的工程方案帧关键工作给可并行的专属池等待工作给低优先级线程高开销销毁工作给独立批处理线程三者共同保证渲染线程raster thread在整条 GPU 提交—完成—回收链路上始终不做等待、不做分配器操作。理解这套分工是后续阅读 Impeller Vulkan 后端代码尤其是UniqueResourceVKT资源句柄与GpuSubmissionTracker提交记账的基础也为排查帧内卡顿、资源泄漏和 shutdown 挂死等问题提供了清晰的定位坐标。【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表