进行嵌套/递归计算时如何运行事件循环?
如何使用settimeout()
打破计算和发布的通常示例似乎依赖于具有浅(1深)调用堆栈的示例。 但是,当您进行深度嵌套或相互恢复的计算(例如树木搜索)时,堆栈中有很多上下文呢?
如果JavaScript具有将“当前延续”封装(即:当前的呼叫堆栈),将其放在事件队列中,然后 return/throw/call返回到顶级事件的函数,那将是理想的选择。循环。 (因此其他事件将运行,然后计算将在其停止的位置重新启动)。我正在寻找一种简单的方法来自愿'屈服'控制,让事件赶上,然后返回控制,然后返回到我们关闭的位置。最好不要重写呼叫链中的每个功能。
但是我找不到任何可以做到这一点的东西...
- 作为退休的计划,我期望呼叫/CC之类的东西,但找不到。
settimeout()
将返回控制[但只有1个级别],然后重新启动一些其他计算(而不是隐式电流 - 登机仪,除非我们将整个应用程序提交给CPS ...)- “收益率”将限制当前功能/堆栈框架的延续,因此可以重新启动, 但是收益率只能返回一个水平。 (收益率类似:返回/CC vs呼叫/CC)
- “投掷”可以将堆栈扔掉,但没有设施可以重新启动 从投掷点开始的计算(我知道;需要诸如'throw/cc'之类的东西),
我使用“屈服”构建了一个半解决方案,但它是klutzy,需要堆栈上的每个功能)被声明为“函数*”,并且(b)在每个调用周围都包含样板代码,以下到下一个功能[以传播收益率并重新启动下一个()]
q:有没有办法在JavaScript中实现此目标而无需仪器呼叫链上的功能?
The usual examples of how to break a computation and release using setTimeout()
seem to rely on having a shallow (1-deep) call stack.
But what about when you are doing a deeply nested or mutually-recursive computation (like a tree search) and you have plenty of context in the stack?
It would be ideal if JavaScript had a function that would encapsulate the 'current continuation' (that is: the current call-stack), put it on the Event Queue and return/throw/call back to the top-level event loop. (so other events would run, and then the computation would be restarted right where it left off). I'm looking for an easy way for a function to voluntarily 'yield' control, let events catch up, and then return control to where we left off. Preferably without re-writing every function in the call-chain.
But I can't find anything that does this...
- As a retired schemer, I'm expecting something like call/cc, but not finding it.
setTimeout()
will return control [but only 1 level up], and restart some other computation (but not the implicit current-continuation, unless we commit the entire application to CPS...)- 'yield' will box the continuation of the current function/stack-frame, so it can be restarted,
but yield only returns one level up. (yield is like: return/cc vs call/cc) - 'throw' can throw way up the stack, but has no facility to restart
the computation from the point of the throw (that I know of; need something like 'throw/cc')
I've built a semi-solution using 'yield', but it is klutzy, requiring every function on the stack to (a) be declared as 'function*' and (b) include boilerplate code around each call down to the next function [to propagate a yield and restart with next()]
Q: Is there a way to achieve this in JavaScript without instrumenting all the functions on the call chain?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我将添加您似乎没有考虑过的替代解决方案:
Promise
s。或更具体地说,要处理承诺的语法糖:async/等待
。实现您的
allowEventloop()
函数很使用
Promise
,容易使用上述函数的简单递归下降解析器的示例(注意:js中的代码,但在ts中执行此操作应该很微不足道):
如您所见,在递归函数的逻辑中,几乎没有更改。它看起来几乎与普通同步版本完全相同。主要区别是您的功能现在返回结果的
Promise
。使用
async/等待
时,您基本上会跳过呼叫堆栈。相反,您真正在做的是使用的链。然后使用()
调用。因此,实际上,呼叫堆栈仍然是1级深的,但是您正在动态构建复杂的。然后()()
链条。在实践中,感觉就像是通常的基于呼叫堆栈的递归。要执行的功能的队列被承诺无形地处理 - 这实际上是处理持续通信风格(CPS)代码的设计模式。这类似于调用堆栈如何管理函数返回的队列。这就是为什么感觉一样的原因。
I'll add an alternative solution that you seem to not have considered:
Promise
s. Or more specifically the syntax sugar for handling promises:async/await
.Using a
Promise
it is simple to implement yourallowEventLoop()
function:Now whenever you need to suspend the current computation and run the event loop you just need to call:
Here's an example of a simple recursive descent parser using the above function (note: code in Js but it should be trivial to do this in Ts):
As you can see, very little is changed in the logic of the recursive function. It looks almost exactly the same as the plain synchronous version. The main difference is that your function now returns a
Promise
of the result.When using
async/await
you basically skip the call stack. Instead what you are really doing is using a chain of.then()
calls. So in reality the call stack is still 1-level deep but you are dynamically constructing a complicated.then()
chain. In practice it feels like the usual call stack based recursion.The queue of functions to execute is handled invisibly by Promises - which is essentially a design pattern for handling Continuation-Passing-Style (CPS) code. This is similar to how the call stack manages the queue of functions to return. This is why it feels the same.
我们希望在长期运行,相互递归的功能调用中启用事件处理。
(例如,递归树搜索)
一定深度或时间后,搜索希望自愿暂停执行
为了允许最高级别事件循环运行(处理鼠标/关键事件,重新绘制图形等),
理想将是runeventloop()的系统级函数
哪个“产生”当前计算,将其自身延续在事件队列中,
并将控制权控制到系统eventloop。
JavaScript似乎仅为此提供部分解决方案:
并且“收益率”将一个值返回到发电机的呼叫者一个级别上的一个级别。
因此,呼叫者必须以发电机的形式具有“延续”。
我们还注意到,尽管未被发现的“投掷”会返回到顶级的控制权,但
JS中没有办法(tiko)恢复&重新启动“投掷”计算。
(从最高级别到相互追回的呼叫到自愿的“产量”)
So:从自愿产量中返回控制,
通过嵌套或相互重新恢复的函数,
一直到系统eventloop,我们执行3件事:
如果是这样,则屈服将“产量”传播到最高级别:
注意:#2不能用用途包装在功能中...因为该功能将受到#1的约束,并且 受#2的约束
settimeout(()=> genr.next())
返回JS Eventloop然后重新启动悬浮函数的链。
[在#2显而易见之前,我编写了这个打字稿代码,如上所示,现在“ fardry”被列为
'
“收益率”提供了有趣/有用的值,并且完成后“返回”信号。
在这个被反转的用例中:产量发出信号,但没有有趣的值,
“返回”提供有趣的计算值。
吸引JS神:
提供一个函数:runeventloop()
透明地将当前延续(完整堆栈)放在事件循环中
并将控制直接返回到顶级。
因此,所有其他来电者和呼叫堆栈
不需要意识到较低级别的暂停/简历。
值得注意的:看来使用这样的发电机有重大的性能。将代码插入以将嵌套发生器从4减少到2后,代码速度更快地运行10倍。因此,也许可以针对复杂/时间敏感的应用程序指示CPS或数据流设计。 (但是,它在开发/调试期间起作用以使KBD/图形进行运行)
另一个注意事项: Chrome施加了最小的“ Settimeout”延迟4ms;因此,如果您计算为1ms,然后以4ms的速度屈服,则可以解释上面的注释。它有助于计算三角洲的最后一个产量到date.now(),并且仅在大于[20-200 ms?]的情况下(取决于您需要的响应程度)。
We want to enable event processing during long-running, mutually recursive function calls.
(for example, a recursive tree search)
After a certain depth or time, the search wants to voluntarily suspend execution
to allow the top level Event Loop to run (handle mouse/key events, repaint graphics, etc)
The ideal would be a system-level function to runEventLoop()
which 'yield' the current computation, put its own continuation on the event queue,
and throw control to the system EventLoop.
It seems that Javascript provides only partial solutions for this:
And 'yield' returns a value to the Generator's caller one level up the call stack.
So that caller must already have the 'continuation' in form of the Generator.
We also note that although an uncaught 'throw' will return control to the top-level,
there is no way (TIKO) in JS to recover & restart the 'thrown' computation.
(from top level through the mutually-recursive calls to the voluntary 'yield')
So: to return control from the voluntary yield,
up through the nested or mutually-recursive functions,
all the way to the system EventLoop, we do 3 things:
and if so, yield itself to propagate the 'yield' to the top level:
Note: #2 cannot usefully be wrapped in a function... because that function would be subject to #1, and the caller of that function is subject to #2
setTimeout(() => genR.next())
return to the JS EventLoopand then restart the chain of suspended functions.
[before #2 was obvious, I wrote this typescript code, now 'yieldR' is inlined, as shown above]
Note: most documented usage of function* are to create a Iterator, a case where
'yield' provides the interesting/useful value, and 'return' signals when done.
In this use-case that is inverted: yield gives a signal, but no interesting value,
and 'return' supplies the interesting computational value.
Appeal to the JS Gods:
Provide a function: runEventLoop()
That transparently puts the current continuation (the full stack) on the event loop
and returns control directly to the top-level.
so all the other callers and the call stack
do not need to be aware of the suspend/resume being done at the lower level.
After note: looks like there is a significant performance hit for using Generators like this. After inlining code to reduce nested Generators from 4 to 2, the code ran 10X faster. So maybe CPS or data-flow design may be indicated for complex/time-sensitive apps. (but still, it worked during dev/debug to get the kbd/graphics going)
Another note: Chrome imposes a minimum 'setTimeout' delay of 4ms; so if you compute for 1ms and then yield for 4ms that is slow and may explain the note above. It helps to compute the delta from last yield until Date.now() and yield only when that is greater than [20 -- 200 ms?] (depending on degree of responsiveness you need).
要重新替代替代方法(数据流/函数 - 标题),请考虑以下方式:
为了保持呼叫堆栈的简短,请将应用程序划分为任务(无递归返回的函数)。
如果您要进行递归调用,则使用:
calllater(()=> recursivetask(arg1,arg2,...))
)并简单地返回。
call-later
将闭合[数据和延续]放在queue
上,顶级可以依次对其进行处理。因此,对于树搜索,在第n层,您会招募任务以在n+1层处处理节点,并加上一个任务来收集和组合结果,然后返回。最终的任务应返回最终结果。该“最终”任务可能包括:
if(queue.length> 0)calllater(finalTask)
,因此它将自己放在队列的末端,直到计算出所有其他子任务,并且已经停止将任务添加到队列中。 [或者,也许您使用一些承诺,并使用finalTask
promise.all(...)]下面的代码还包括循环中的计时器,以便运行许多任务,直到超过阈值(以及返回JavaScript事件循环)
To reify the alternative (data-flow/function-queue) approach, consider this:
To keep the call-stack short, divide the application into tasks (functions that return without recursion).
If you would make a recursive call, then instead use:
callLater(()=>recursiveTask(arg1, arg2, ...))
and simply return.
callLater
puts the closure [data and continuation] on thequeue
where the top-level can process it in turn.So for a tree search, at layer N, you enqueue tasks to process the nodes at layer N+1, PLUS a task to gather and combine the results, then return. The final task enqueued should return the final result. That 'final' task will likely include something like:
if (queue.length > 0) callLater(finalTask)
so it puts itself to the end of the queue until all the other subtasks have been computed and have stopped adding tasks to the queue. [or maybe you use some Promises and trigger thefinalTask
withPromise.all(...)
]The code below also includes a timer in the loop, so as to run a number of tasks until a threshold is exceeded (and the return to the JavaScript Event Loop)