跳转到内容
搜索文档

生命周期

最后更新 查看 MarkdownAgent 设置

生命周期、内存与资源管理

使用服务绑定通过 RPC 调用另一个 Worker 时,会占用被调用 Worker 的内存。考虑以下示例:

let user = await env.USER_SERVICE.findUser(id);

假设服务端 findUser() 返回继承 RpcTarget 的对象,因此客户端的 user 最终是指向该远程对象的 stub。

只要客户端上仍存在 stub,服务端上的对应对象就无法被垃圾回收。但每个 isolate 都有自己的垃圾回收器,无法查看其他 isolate。因此,为了让服务端的 isolate 知道对象可以被回收,调用方 isolate 必须发送明确的信号,称为「释放(disposing)」stub。

在许多情况下(如下所述),系统会自动识别 stub 何时不再需要并自动释放。但为获得最佳性能,代码应在不再使用 stub 时显式释放。

显式资源管理(Explicit Resource Management)

为确保资源得到正确释放,应使用显式资源管理(Explicit Resource Management),这是一项新的 JavaScript 语言特性,允许你明确指示何时可以释放资源。显式资源管理是 TC39 第 3 阶段提案——即将进入 V8

显式资源管理添加了以下语言特性:

如果变量使用 using 声明,当变量超出作用域时,将调用该变量的释放器。例如:

function sendEmail(id, message) {
  using user = await env.USER_SERVICE.findUser(id);
  await user.sendEmail(message);

  // user[Symbol.dispose]() is implicitly called at the end of the scope.
}

using 声明有助于确保不会忘记释放 stub——即使代码被异常中断。

如何在 Worker 中使用 using 声明

Wrangler v4+ 原生支持 using 关键字。如果你使用的是较早版本的 Wrangler,则需要手动释放资源。

以下代码:

{
	using counter = await env.COUNTER_SERVICE.newCounter();
	await counter.increment(2);
	await counter.increment(4);
}

...等价于:

{
	const counter = await env.COUNTER_SERVICE.newCounter();
	try {
		await counter.increment(2);
		await counter.increment(4);
	} finally {
		counter[Symbol.dispose]();
	}
}

自动释放与执行上下文

RPC 系统在以下情况下会自动释放 stub:

事件处理程序 / 执行上下文结束

当事件处理程序「完成」时,作为该事件一部分创建的所有 stub 会自动释放。

例如,考虑处理传入 HTTP 事件的 fetch() 处理程序。处理程序可能在处理事件时发起出站 RPC,这些 RPC 可能返回 stub。当最终 HTTP 响应发送后,处理程序「完成」,所有 stub 会立即释放。

更精确地说,事件有一个「执行上下文(execution context)」,从处理程序首次调用时开始,到 HTTP 响应发送时结束。如果客户端在收到响应之前断开连接,执行上下文也可能提前结束;或者通过调用 ctx.waitUntil() 可以延长其正常结束点。

例如,下面的 Worker 未使用 using 声明,但一旦 fetch() 处理程序返回响应,stub 就会被释放:

export default {
	async fetch(request, env, ctx) {
		let authResult = await env.AUTH_SERVICE.checkCookie(
			req.headers.get("Cookie"),
		);
		if (!authResult.authorized) {
			return new Response("Not authorized", { status: 403 });
		}
		let profile = await authResult.user.getProfile();

		return new Response(`Hello, ${profile.name}!`);
	},
};

通过 RPC 调用的 Worker 也有执行上下文。当调用 WorkerEntrypoint 上的 RPC 方法时,上下文开始。如果此 RPC 的参数或结果中未传递任何 stub,则 RPC 返回时上下文结束(事件「完成」)。但是,如果传递了任何 stub,则执行上下文会隐式延长,直到所有此类 stub 都被释放(以及通过它们发起的所有调用都已返回)。与 HTTP 一样,如果客户端断开连接,服务端的执行上下文会立即取消,无论 stub 是否仍然存在。作为另一个 Worker 的客户端,在其自身执行上下文结束时被视为已断开连接。同样,可以通过 ctx.waitUntil() 延长上下文。

作为 RPC 调用参数接收的 stub

当 stub 作为 RPC 参数接收时,调用返回时会自动释放。如果你希望保留 stub 更长时间,必须对其调用 dup() 方法。

释放 RPC 对象会释放属于该对象的 stub

当 RPC 返回任何类型的对象时,系统会为该对象添加释放器。释放它会释放调用返回的所有 stub。例如,如果 RPC 返回包含四个 stub 的数组,数组本身会有一个释放器来释放全部四个 stub。RPC 返回值没有释放器的唯一情况是原始值,如数字或字符串。这些类型无法添加释放器,但由于它们本身不能包含 stub,因此在此情况下不需要释放器。

这意味着你几乎应始终将 RPC 的结果存储到 using 声明中:

using result = stub.foo();

这样,如果结果包含任何 stub,它们会被释放。即使你不期望 RPC 返回 stub,如果它返回任何类型的对象,将其存储到 using 声明中也是好主意。这样,如果将来 RPC 扩展为返回 stub,你的代码已做好准备。

如果你决定在 using 声明作用域之外保留返回的 stub,可以在作用域结束前对其调用 dup()。(请记住稍后显式释放副本。)

释放器与 RpcTarget

继承 RpcTarget 的类可以选择实现释放器:

class Foo extends RpcTarget {
	[Symbol.dispose]() {
		// ...
	}
}

RpcTarget 的释放器在最后一个 stub 被释放后运行。请注意,客户端对 stub 释放器的调用不会等待服务端释放器被调用;服务端释放器稍后才被调用。因此,释放器抛出的任何异常不会传播到客户端;而是作为未捕获异常报告。请注意,RpcTarget 的释放器必须声明为 Symbol.dispose。不支持 Symbol.asyncDispose

dup() 方法

有时,你需要将 stub 传递给会在使用完毕后释放 stub 的函数,但你也想保留 stub 以供后续使用。为解决此问题,可以「dup」stub:

let stub = await env.SOME_SERVICE.getThing();

// Create a duplicate.
let stub2 = stub.dup();

// Call some function that will dispose the stub.
await func(stub);

// stub2 is still valid

你可以将 dup() 视为与同名 Unix 系统调用类似:它创建一个指向同一目标的新句柄,必须独立关闭(释放)。

如果 stub 指向的 RpcTarget实例具有释放器,则只有在所有副本都被释放后才会调用释放器。但这仅适用于源自同一 stub 的副本。如果同一 RpcTarget 实例多次通过 RPC 传递,每次都会创建新 stub,这些 stub 不被视为彼此的副本。因此,释放器会在 RpcTarget 每次被发送时调用一次。

为避免这种情况,你可以在本地手动创建 stub,然后多次通过 RPC 传递该 stub。通过 RPC 传递 stub 时,stub 的所有权会转移给接收方,因此每次发送时都必须 dup()

import { RpcTarget, RpcStub } from "cloudflare:workers";

class Foo extends RpcTarget {
	// ...
}

let obj = new Foo();
let stub = new RpcStub(obj);
await rpc1(stub.dup()); // sends a dup of `stub`
await rpc2(stub.dup()); // sends another dup of `stub`
stub[Symbol.dispose](); // disposes the original stub

// obj's disposer will be called when the other two stubs
// are disposed remotely.

这篇文档对您有帮助吗?