Worker 的生命周期和 WorkerRefs¶
Worker 作为一种线程编程模型,被引入 Web 世界以有效利用 Web 世界中的计算能力。就像常规的线程编程一样,Worker 可以根据需要随时创建和删除。
由于 Worker 可以随时被删除,因此在 Workers 上开发 API 时,在处理关闭行为时应格外小心。否则,内存问题、UAF、内存泄漏或关闭挂起将不足为奇。此外,调试 Workers 上的这些问题有时并不容易。崩溃堆栈可能无法提供非常有用的信息。由于可能是线程交错问题,因此错误有时需要特殊的序列才能重现。为了避免遇到这些麻烦,牢记 Worker 的生命周期以及如何使用 WorkerRefs 将非常有帮助。
Worker 生命周期¶
Worker 的生命周期由 WorkerPrivate 类中的状态机维护。Worker 可能处于以下状态
Pending(挂起)
Running(运行)
Closing(关闭)
Canceling(取消)
Killing(终止)
Dead(死亡)
下面我们简要描述每个状态的作用。
Pending(挂起)¶
这是 Worker 的初始状态。
Worker 的初始化在此状态下在父线程(主线程或父 Worker)和 Worker 线程中完成。
Worker 的初始化从其父线程开始,包括:
从父窗口/Worker 获取 WorkerLoadInfo
为 Worker 创建一个 WorkerPrivate
在 RuntimeService 对象中注册 Worker
为 Worker 初始化一个线程(Worker 线程),并在 Worker 线程上分派 WorkerThreadPrimaryRunnable
连接调试器
将 CompileScriptRunnable 分派到 Worker 线程
在 Worker 线程开始运行 Runnable 之前,Worker 可能已经暴露给其父窗口/Worker。因此,父窗口/Worker 可以通过 postMessage() 方法向 Worker 发送消息。如果 Worker 尚未处于“Running”状态,则这些 Runnable 将保留在 WorkerPrivate::mPreStartRunnables 中。
当 WorkerThreadPrimaryRunnable 在 Worker 线程上开始执行时,它会在 Worker 线程上继续初始化,包括:
建立 WorkerPrivate 和 Worker 线程之间的连接。然后将 WorkerPrivate::mPreStartRunnables 移动到 Worker 线程的事件队列。
为 Worker 初始化 PerformanceStorage。
启动 Worker 的 Cycle-Collector。
为 Worker 初始化 JS 上下文。
调用 WorkerPrivate::DoRunLoop() 以使用 Worker 线程事件队列中的 Runnable。
Running(运行)¶
这是 Worker 开始在 Worker 线程上执行 Runnable 的状态。
一旦 Worker 进入“Running”状态,
启用内存报告器
启动 GC 定时器。
“Running”是我们使用 Worker 的状态。此时,我们可以:
创建 WorkerRefs 以获取 Worker 关闭通知,并通过注册的回调运行关闭清理作业。
创建同步事件循环,使 Worker 线程等待另一个线程的执行。
将事件分派到 WorkerGlobalScope 以触发脚本中定义的事件回调。
我们将在后面详细讨论 WorkerRef 和同步事件循环。
Closing(关闭)¶
当调用 DedicateWorkerGlobalScope.close()/SharedWorkerGlobalScope.close() 时,这是 DedicatedWorker 和 SharedWorker 的特殊状态。
当 Worker 进入“Closing”状态时,
取消 Worker 的所有超时/时间间隔。
不允许在 WorkerGlobalScope 上使用 BroadcastChannel.postMessage()。
Worker 将保持在“Closing”状态,直到 Worker 的所有同步事件循环都关闭。
Canceling(取消)¶
当 Worker 进入“Canceling”状态时,它开始 Worker 关闭步骤。
将 WorkerGlobalScope(nsIGlobalObject)设置为正在死亡。
这意味着事件不会分派到 WorkerGlobalScope,并且挂起的已分派事件的回调不会执行。
取消 Worker 的所有超时/时间间隔。
通知 WorkerRef 持有者和子 Worker。
因此,WorkerRef 持有者和子 Worker 将开始关闭作业
立即中止脚本。
一旦所有同步事件循环都关闭,
断开 WorkerGlobalScope 的 EventTarget/WebTaskScheduler 连接
Killing(终止)¶
这是开始销毁 Worker 的状态
关闭 GC 定时器
禁用内存报告器
将状态切换到“Dead”
取消并释放剩余的 WorkerControlRunnables
退出 WorkerPrivate::DoRunLoop()
Dead(死亡)¶
Worker 退出主事件循环,它继续关闭过程
释放剩余的 WorkerDebuggerRunnables
取消根 WorkerGlobalScope 和 WorkerDebugGlobalScope
触发 GC 以释放 GlobalScopes
关闭 Worker 的 Cycle-Collector
将 TopLevelWorkerFinishRunnable/WorkerFinishRunnable 分派到父线程
禁用/断开 WorkerDebugger 连接
在 RuntimeService 对象中注销 Worker
释放 WorkerPrivate::mSelf 和 WorkerPrivate::mParentEventTargetRef
WorkerPrivate 应该在其自身引用被取消后释放。
将 FinishedRunnable 分派到主线程以释放 Worker 线程。
如何关闭 Worker¶
通常,有四种情况会导致 Worker 进入关闭状态。
Worker 被 GC/CC。
导航到另一个页面。
Worker 空闲一段时间。(请注意,空闲不是 Worker 的状态,它是“Running”状态下的条件)
在 Worker 的脚本中调用 self.close()。
在父脚本中调用 Worker.terminate()。
Firefox 关闭。
Worker 状态流程图¶
此流程图显示了 Worker 状态如何变化。
当 WorkerThreadPrimaryRunnable 在 Worker 线程上调用 WorkerPrivate::DoRunLoop 时,状态从“Pending”更改为“Running”。如果 Firefox 关闭发生在进入“Running”之前,则状态会直接从“Pending”更改为“Dead”。
当 Worker 处于“Running”状态时,状态更改必须由请求 Worker 关闭导致。对于 Worker 的脚本调用 self.close() 的特殊情况,状态切换到“Closing”。否则,状态切换到“Canceling”。当所有同步事件循环完成后,“Closing”状态的 Worker 将切换到“Canceling”。
当满足以下要求时,“Canceling”状态的 Worker 会将其状态切换到“Killing”。
没有 WorkerRefs、没有子 Worker、没有超时和没有同步事件循环
Worker 线程主事件队列、控制 Runnable 和调试器 Runnable 没有挂起的 Runnable
状态会自动从“Killing”切换到“Dead”。
WorkerRefs¶
由于 Worker 的关闭可能随时发生,因此了解关闭何时开始对于开发至关重要,尤其是在释放资源并在 Worker 关闭阶段完成操作时。因此,引入了 WorkerRefs 来获取 Worker 关闭的通知。当 Worker 进入“Canceling”状态时,它会通知相应的 WorkerRefs 在 Worker 线程上执行注册的回调。WorkerRef 持有者在注册的回调中同步或异步地完成其关闭步骤,然后释放 WorkerRef。
根据以下要求,引入了四种类型的 WorkerRefs。
WorkerRef 是否应阻塞 Worker 的关闭
WorkerRef 是否应阻塞 Worker 上的循环收集
WorkerRef 是否需要在其他线程上持有。
WeakWorkerRef¶
WeakWorkerRef 顾名思义是一个“弱”引用,因为 WeakWorkerRef 在 WeakWorkerRef 的注册回调执行完成后会立即释放对 Worker 的内部引用。因此,WeakWorkerRef 不会阻塞 Worker 的关闭。此外,持有 WeakWorkerRef 不会阻塞 GC/CC Worker。这意味着即使有指向 Worker 的 WeakWorkerRefs,Worker 也会被视为已循环收集。
WeakWorkerRef 是引用计数的,但不是线程安全的。
WeakWorkerRef 旨在仅获取 Worker 的关闭通知并同步完成关闭步骤。
StrongWorkerRef¶
与 WeakWorkerRef 不同,StrongWorkerRef 在回调执行后不会释放其对 Worker 的内部引用。StrongWorkerRef 的内部引用在 StrongWorkerRef 被销毁时释放。这意味着 StrongWorkerRef 允许其持有者通过置空 StrongWorkerRef 来确定何时释放 Worker。这也使得 StrongWorkerRef 的持有者阻塞 Worker 的关闭。
使用 StrongWorkerRef 时,资源清理可能涉及多个线程和异步行为。StrongWorkerRef 的释放时机至关重要,以避免出现内存问题,例如 UAF 或泄漏。必须释放 StrongWorkerRef。否则,关闭挂起将不足为奇。
StrongWorkerRef 还会阻塞 GC/CC Worker。一旦有指向 Worker 的 StrongWorkerRef,GC/CC 将不会收集 Worker。
StrongWorkerRef 是引用计数的,但不是线程安全的。
ThreadSafeWorkerRef¶
ThreadSafeWorkerRef 是 StrongWorkerRef 的扩展。不同之处在于 ThreadSafeWorkerRef 持有者可以位于另一个线程上。由于它是 StrongWorkerRef 的扩展,因此它具有与 StrongWorkerRef 相同的特性。这意味着它的持有者会阻塞 Worker 的关闭,并且它还会阻塞 GC/CC Worker。
使用 ThreadSafeWorkerRef 时,就像 StrongWorkerRef 一样,ThreadSafeWorkerRef 的释放时机对于内存问题至关重要。除了释放时机之外,还应注意回调是在 Worker 线程上执行,而不是在持有者的拥有线程上执行。
ThreadSafeWorkerRef 是引用计数的且线程安全的。
IPCWorkerRef¶
IPCWorkerRef 是一种特殊的 WorkerRef,用于将 IPC actor 的生命周期与其 Worker 关闭通知绑定。(在我们当前的代码库中,Cache API 和 Client API 使用 IPCWorkerRef)
因为某些 IPC 关闭需要在 Worker 关闭期间按照特定的顺序进行。但是,为了进行这些 IPC 关闭,需要确保 Worker 保持活动状态,因此 IPCWorkerRef 会阻塞 Worker 的关闭。但 IPC 关闭不需要阻塞 GC/CC Worker。
IPCWorkerRef 是引用计数的,但不是线程安全的。
以下是 WorkerRefs 之间比较的表格
WeakWorkerRef | StrongWorkerRef | ThreadSafeWorkerRef | IPCWorkerRef | |
持有者线程 | Worker 线程 | Worker 线程 | 任何线程 | Worker 线程 |
回调执行线程 | Worker 线程 | Worker 线程 | Worker 线程 | Worker 线程 |
阻塞 Worker 的关闭 | 否 | 是 | 是 | 是 |
阻塞 GC Worker | 否 | 是 | 是 | 否 |
WorkerRef 回调¶
创建 WorkerRef 时可以注册 WorkerRef 回调。回调负责释放与 WorkerRef 持有者相关的资源。例如,解决/拒绝由 WorkerRef 持有者创建的 Promise。清理行为可能是同步的或异步的,具体取决于所涉及的功能的复杂程度。例如,Cache API 可能需要等到操作在 IO 线程上完成,并在主线程上释放仅限主线程的对象。
为了避免内存问题,对于 WorkerRef 回调,需要牢记一些事项
在完成清理步骤之前不要释放 WorkerRef。(UAF)
不要忘记释放相关的资源。(内存泄漏)
不要忘记释放 WorkerRef(StrongWorkerRef/ThreadWorkerRef/IPCWorkerRef)(关闭挂起)