【发布时间】:2021-07-03 00:27:01
【问题描述】:
背景
我正在尝试将 elixir 的 actor 模型 语言原语移植到 JS 中。我想出了一个解决方案(在 JS 中)来模拟 receive elixir 关键字,使用“接收器”函数和生成器。
这是一个简化的实现和演示,向您展示这个想法。
API:
type ActorRef: { send(msg: any): void }
type Receiver = (msg: any) => Receiver
/**
* `spawn` takes a `initializer` and returns an `actorRef`.
* `initializer` is a factory function that should return a `receiver` function.
* `receiver` is called to handle `msg` sent through `actorRef.send(msg)`
*/
function spawn(initializer: () => Receiver): ActorRef
演示:
function* coroutine(ref) {
let result
while (true) {
const msg = yield result
result = ref.receive(msg)
}
}
function spawn(initializer) {
const ref = {}
const receiver = initializer()
ref.receive = receiver
const gen = coroutine(ref)
gen.next()
function send(msg) {
const ret = gen.next(msg)
const nextReceiver = ret.value
ref.receive = nextReceiver
}
return { send }
}
function loop(state) {
console.log('current state', state)
return function receiver(msg) {
if (msg.type === 'ADD') {
return loop(state + msg.value)
} else {
console.log('unhandled msg', msg)
return loop(state)
}
}
}
function main() {
const actor = spawn(() => loop(42))
actor.send({ type: 'ADD', value: 1 })
actor.send({ type: 'BLAH', value: 1 })
actor.send({ type: 'ADD', value: 1 })
return actor
}
window.actor = main()
关注
以上模型有效。但是我有点担心这种方法对性能的影响,我不清楚它创建的所有闭包上下文对内存的影响。
function loop(state) {
console.log('current state', state) // <--- `state` in a closure context <─┐ <─────┐
return function receiver(msg) { // ---> `receiver` closure reference ──┘ │
if (msg.type === 'ADD') { │
return loop(state + msg.value) // ---> create another context that link to this one???
} else {
console.log('unhandled msg', msg)
return loop(state)
}
}
}
loop 是返回“接收者”的“初始化器”。为了保持内部状态,我将它(state 变量)保存在“接收器”函数的闭包上下文中。
当前接收者收到消息后,可以修改内部状态,并将其传递给loop,递归创建一个新接收者替换当前接收者。
显然,新的接收者也有一个新的闭包上下文来保持新的状态。在我看来,这个过程可能创建了一个阻止 GC 的深层链接上下文对象链?
我知道闭包引用的上下文对象可以在某些情况下被链接。如果它们是链接的,那么它们显然不会在最里面的闭包被释放之前被释放。据this articleV8优化在这方面很保守,图片不好看。
问题
如果有人能回答这些问题,我将不胜感激:
-
loop示例是否创建了深度链接的上下文对象? - 在这个例子中上下文对象的生命周期是什么样的?
- 如果当前示例没有,这种
receiver创建receiver机制是否最终会在其他情况下创建深度链接的上下文对象? - 如果对问题 3 回答“是”,您能否举一个例子来说明这种情况?
跟进 1
@TJCrowder 的后续问题。
闭包是词法的,因此它们的嵌套遵循源代码的嵌套。
说得好,这很明显,但我错过了????
只是想确认我的理解是正确的,举一个不必要的复杂例子(请多多包涵)。
这两个在逻辑上是等价的:
// global context here
function loop_simple(state) {
return msg => {
return loop_simple(state + msg.value)
}
}
// Notations:
// `c` for context, `s` for state, `r` for receiver.
function loop_trouble(s0) { // c0 : { s0 }
// return r0
return msg => { // c1 : { s1, gibberish } -> c0
const s1 = s0 + msg.value
const gibberish = "foobar"
// return r1
return msg => { // c2 : { s2 } -> c1 -> c0
const s2 = s1 + msg.value
// return r2
return msg => {
console.log(gibberish)
// c3 is not created, since there's no closure
const s3 = s2 + msg.value
return loop_trouble(s3)
}
}
}
}
但是对内存的影响是完全不同的。
- 步入
loop_trouble,c0被创建持有s0;返回r0 -> c0。 - 进入
r0,创建c1,持有s1和gibberish,返回r1 -> c1。 - 步入
r1,创建c2,持有s2,返回r2 -> c2
我相信在上述情况下,当r2(最里面的箭头函数)被用作“当前接收者”时,实际上不仅仅是r2 -> c2,而是r2 -> c2 -> c1 -> c0,所有三个上下文对象都被保留(如果我在这里已经错了,请纠正我。
问题:哪种情况是真的?
- 之所以保留所有三个上下文对象,仅仅是因为我故意在其中放入了
gibberish变量。 - 或者即使我删除了
gibberish,它们也会被保留。也就是说s1 = s0 + msg.value的依赖就足够链接c1 -> c0了。
跟进 2
因此,作为“容器”的环境记录始终被保留,因为容器中包含的“内容”可能因引擎而异,对吧?
一种非常幼稚的未优化方法可能会盲目地将所有局部变量以及arguments 和this 包含到“内容”中,因为规范没有说明任何关于优化的内容。
更聪明的方法可以是查看嵌套函数并检查确切需要什么,然后决定将什么包含到内容中。这在article I linked 中被称为“促销”,但这条信息可以追溯到 2013 年,恐怕它可能已经过时了。
您是否有更多关于此主题的最新信息要分享?我对 V8 如何实现这种策略特别感兴趣,因为我目前的工作严重依赖于电子运行时。
【问题讨论】:
-
P.S.我保证向提供有用信息的人提供 500 赏金。目前我还不能开始。
-
下面的答案没用吗?
-
@T.J.Crowder 是的!非常感谢!我正忙着给你写一个后续问题,请耐心等待。
标签: javascript garbage-collection closures v8