进行嵌套/递归计算时如何运行事件循环?

发布于 2025-01-23 03:34:40 字数 801 浏览 4 评论 0原文

如何使用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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

与酒说心事 2025-01-30 03:34:40

我将添加您似乎没有考虑过的替代解决方案:Promise s。或更具体地说,要处理承诺的语法糖:async/等待

实现您的allowEventloop()函数很

function allowEventLoop () {
    return new Promise((ok,fail) => setTimeout(ok,0));
}

使用Promise

await allowEventLoop();

容易使用上述函数的简单递归下降解析器的示例(注意:js中的代码,但在ts中执行此操作应该很微不足道):

async function drawTree(node, indent) {
    if (!indent) indent = 0;

    let tree = `${'\t'.repeat(indent)}${node.name}\n`;

    await allowEventLoop();

    if (node.children) {
        for (let child of node.children) {
            tree += await drawTree(child, indent+1);
        }
    }

    return tree;
}

如您所见,在递归函数的逻辑中,几乎没有更改。它看起来几乎与普通同步版本完全相同。主要区别是您的功能现在返回结果的Promise

使用async/等待时,您基本上会跳过呼叫堆栈。相反,您真正在做的是使用的链。然后使用()调用。因此,实际上,呼叫堆栈仍然是1级深的,但是您正在动态构建复杂的。然后()()链条。在实践中,感觉就像是通常的基于呼叫堆栈的递归。

要执行的功能的队列被承诺无形地处理 - 这实际上是处理持续通信风格(CPS)代码的设计模式。这类似于调用堆栈如何管理函数返回的队列。这就是为什么感觉一样的原因。

I'll add an alternative solution that you seem to not have considered: Promises. Or more specifically the syntax sugar for handling promises: async/await.

Using a Promise it is simple to implement your allowEventLoop() function:

function allowEventLoop () {
    return new Promise((ok,fail) => setTimeout(ok,0));
}

Now whenever you need to suspend the current computation and run the event loop you just need to call:

await allowEventLoop();

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):

async function drawTree(node, indent) {
    if (!indent) indent = 0;

    let tree = `${'\t'.repeat(indent)}${node.name}\n`;

    await allowEventLoop();

    if (node.children) {
        for (let child of node.children) {
            tree += await drawTree(child, indent+1);
        }
    }

    return tree;
}

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.

若相惜即相离 2025-01-30 03:34:40

我们希望在长期运行,相互递归的功能调用中启用事件处理。
(例如,递归树搜索)
一定深度或时间后,搜索希望自愿暂停执行
为了允许最高级别事件循环运行(处理鼠标/关键事件,重新绘制图形等),

理想将是runeventloop()的系统级函数
哪个“产生”当前计算,将其自身延续在事件队列中,
并将控制权控制到系统eventloop。

JavaScript似乎仅为此提供部分解决方案:

  • “ Settimeout()”将在事件队列(但不是当前的延续)
  • “收益率”中放置一个功能,而不会将当前延续暂停,但不要将其放在事件队列中。
    并且“收益率”将一个值返回到发电机的呼叫者一个级别上的一个级别。
    因此,呼叫者必须以发电机的形式具有“延续”。

我们还注意到,尽管未被发现的“投掷”会返回到顶级的控制权,但
JS中没有办法(tiko)恢复&重新启动“投掷”计算。
(从最高级别到相互追回的呼叫到自愿的“产量”)

So:从自愿产量中返回控制,
通过嵌套或相互重新恢复的函数,
一直到系统eventloop,我们执行3件事:

  1. 每个功能[呼叫者&必须将]声明为函数*(以便可以产生)
  2. 每个功能[呼叫者]必须测试其[调用]后代是否被暂停,
    如果是这样,则屈服将“产量”传播到最高级别:
    let result, genR = calledStarFunction(args);
       while (result = genR.next(), !result.done) yield;
       use (result.value)

注意:#2不能用用途包装在功能中...因为该功能将受到#1的约束,并且 受#2的约束

  1. 的呼叫者函数在顶级处 ,使用settimeout(()=> genr.next())返回JS Eventloop
    然后重新启动悬浮函数的链。

[在#2显而易见之前,我编写了这个打字稿代码,如上所示,现在“ fardry”被列为

    /** <yield: void, return: TReturn, yield-in: unknown> */
    export type YieldR<TReturn> = Generator<void, TReturn, unknown>
    /**
     * Top-level function to give control to JS Event Loop, and then restart the stack of suspended functions.
     * 'genR' will restart the first/outermost suspended block, which will have code like *yieldR()
     * that loops to retry/restart the next/inner suspended function.
     * @param genR 
     * @param done 
     */
    export function allowEventLoop<T>(genR: YieldR<T>, done?: (result: T) => void): void  {
      let result = genR.next()
      if (result.done) done && done(result.value)
      else setTimeout(() => allowEventLoop(genR, done))
    }
    /** 
     * Return next result from genR. 
     * If genR returns an actual value, return that value
     * If genR yields<void> then propagate a 'yield' to each yieldR up to allowEventLoop(); 
     * 
     * This shows the canonical form of the code.
     * It's not useful to actually *call* this code since it also returns a Generator,
     * and the calling code must then write a while loop to handle the yield-vs-return!
     */
    export function* yieldR<T extends object> (genR: YieldR<T>, log?:string) {
      let result: IteratorResult<void, T>
      while (result = genR.next(), !result.done) yield
      return result.value
    }

'
“收益率”提供了有趣/有用的值,并且完成后“返回”信号。
在这个被反转的用例中:产量发出信号,但没有有趣的值,
“返回”提供有趣的计算值。

吸引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:

  • 'setTimeout()' will put a function on the event queue [but not the current continuation]
  • 'yield' will suspend the current continuation, but not put it on the event queue.
    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:

  1. Each function [caller & called] must be declared as function* (so it can yield)
  2. Each function [caller] must test whether its [called] descendant suspended,
    and if so, yield itself to propagate the 'yield' to the top level:
    let result, genR = calledStarFunction(args);
       while (result = genR.next(), !result.done) yield;
       use (result.value)

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

  1. At the top-level, use setTimeout(() => genR.next()) return to the JS EventLoop
    and then restart the chain of suspended functions.

[before #2 was obvious, I wrote this typescript code, now 'yieldR' is inlined, as shown above]

    /** <yield: void, return: TReturn, yield-in: unknown> */
    export type YieldR<TReturn> = Generator<void, TReturn, unknown>
    /**
     * Top-level function to give control to JS Event Loop, and then restart the stack of suspended functions.
     * 'genR' will restart the first/outermost suspended block, which will have code like *yieldR()
     * that loops to retry/restart the next/inner suspended function.
     * @param genR 
     * @param done 
     */
    export function allowEventLoop<T>(genR: YieldR<T>, done?: (result: T) => void): void  {
      let result = genR.next()
      if (result.done) done && done(result.value)
      else setTimeout(() => allowEventLoop(genR, done))
    }
    /** 
     * Return next result from genR. 
     * If genR returns an actual value, return that value
     * If genR yields<void> then propagate a 'yield' to each yieldR up to allowEventLoop(); 
     * 
     * This shows the canonical form of the code.
     * It's not useful to actually *call* this code since it also returns a Generator,
     * and the calling code must then write a while loop to handle the yield-vs-return!
     */
    export function* yieldR<T extends object> (genR: YieldR<T>, log?:string) {
      let result: IteratorResult<void, T>
      while (result = genR.next(), !result.done) yield
      return result.value
    }

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).

夜夜流光相皎洁 2025-01-30 03:34:40

要重新替代替代方法(数据流/函数 - 标题),请考虑以下方式:
为了保持呼叫堆栈的简短,请将应用程序划分为任务(无递归返回的函数)。
如果您要进行递归调用,则使用:calllater(()=&gt; recursivetask(arg1,arg2,...))
并简单地返回。
call-later将闭合[数据和延续]放在queue上,顶级可以依次对其进行处理。

因此,对于树搜索,在第n层,您会招募任务以在n+1层处处理节点,并加上一个任务来收集和组合结果,然后返回。最终的任务应返回最终结果。该“最终”任务可能包括:if(queue.length&gt; 0)calllater(finalTask​​),因此它将自己放在队列的末端,直到计算出所有其他子任务,并且已经停止将任务添加到队列中。 [或者,也许您使用一些承诺,并使用finalTask​​ promise.all(...)]

下面的代码还包括循环中的计时器,以便运行许多任务,直到超过阈值(以及返回JavaScript事件循环)

type FUNC<T> = ()=>T
const callQueue: Array<FUNC<any>> = []
function callLater(fun: FUNC<any>) {
  callQueue.push(fun)
}
function topLevel<T>(start: FUNC<T>, done?: (value: T) => void, threshold = 30, ms0 = Date.now()) {
  var dms: number
  while ((dms = Date.now() - ms0) < threshold) {
    let value = start()    // which may invoke callLater() to enqueue more tasks
    if (callQueue.length == 0) return done && done(value)
  }
  setTimeout(() => topLevel(callQueue.shift(), done, threshold))
}

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 the queue 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 the finalTask with Promise.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)

type FUNC<T> = ()=>T
const callQueue: Array<FUNC<any>> = []
function callLater(fun: FUNC<any>) {
  callQueue.push(fun)
}
function topLevel<T>(start: FUNC<T>, done?: (value: T) => void, threshold = 30, ms0 = Date.now()) {
  var dms: number
  while ((dms = Date.now() - ms0) < threshold) {
    let value = start()    // which may invoke callLater() to enqueue more tasks
    if (callQueue.length == 0) return done && done(value)
  }
  setTimeout(() => topLevel(callQueue.shift(), done, threshold))
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文