【问题标题】:How do I write an app using a recursive function that can be started / stopped如何使用可以启动/停止的递归函数编写应用程序
【发布时间】:2017-02-15 18:58:35
【问题描述】:

我有一个需要连续运行功能的应用程序。该函数返回一个承诺。我希望应用程序等到承诺解决后再启动该功能。

此外,我的应用程序需要一个 startstop 函数来分别启动或停止该函数。

我这里有一个简化的例子:

class App {
  constructor() {
    this.running = false
    this.cycles = 0
  }

  start() {
    this.running = true
    this._run()
  }

  stop() {
    this.running = false
  }

  _run() {
    Promise.resolve().then(() => {
      this.cycles++
    }).then(() => {
      if (this.running) {
        this._run()
      }
    })
  }
}

module.exports = App

我的问题是,当我使用这个时,setTimeout 似乎放弃了我。例如,如果我运行这个:

const App = require("./app")

const a = new App()

a.start()

console.log("a.start is not blocking...")

setTimeout(() => {
  console.log("This will never get logged")
  a.stop()
  console.log(a.cycles)
}, 500)

输出将是:

a.start is not blocking...

然后setTimeout 回调中的代码永远不会被调用。

我可以尝试在命令行上开始运行 node 并直接在 REPL 中输入,但在我调用 a.start() 后,终端冻结,我无法再输入任何内容。

这种事情看起来应该是很常见的模式。例如,Express 允许您启动/停止服务器而不会出现这些问题。我需要做什么才能获得这种行为?

【问题讨论】:

  • 这种类型的应用程序称为“服务”。我知道在 .Net 世界中,您可以创建一个类型为“Windows 服务”的项目,然后将其注册为任何计算机上的服务,使用控制面板/管理工具/服务停止/启动它。我认为你应该调查一下。

标签: javascript node.js asynchronous recursion promise


【解决方案1】:

阻塞递归Promises

这实际上是与递归相关的“阻塞”Promise 的一个很好的例子。

您的第一个console.log 调用按预期执行。这是一个同步操作,由于 Javascript 中的 run-to-completion 调度,它保证在 event loop 的当前迭代中运行。

您的第二个console.log 是异步的,由于setTimeout 的实现,它被附加到事件循环的下一次迭代 的队列中。但是,永远不会到达下一次迭代,因为Promise.resolve().then(...)then 回调附加到当前迭代 的队列末尾。由于这是递归完成的,因此当前的迭代永远不会完成。

因此,您的第二个 log(在事件循环的下一回合排队)永远不会记录。

使用 node.js,您可以通过使用 setImmediate 实现递归函数来重现此行为:

// watch out: this functions is blocking!

function rec(x) {
  return x === Infinity
   ? x
   : (console.log(x), setImmediate(rec, x + 1));
}

Bergi 的实现只是通过将 setTimeout 应用于解析回调来绕过正常的异步 Promise 行为。

【讨论】:

    【解决方案2】:

    您的_run() 方法是无限的。除非其他代码可以运行并更改 this.running 的值,否则它永远不会停止调用自己,但是仅使用 .then() 不足以可靠地允许您的其他 setTimeout() 代码运行,因为 .then() 以比计时器更高的优先级运行事件队列中的事件。

    虽然.then() 保证是异步的,但它将以比setTimeout() 更高的优先级运行,这意味着您的递归调用只会无限期运行,而您的其他setTimeout() 永远不会运行,因此this.running 永远不会改变.

    如果相反,您使用简短的 setTimeout() 本身递归调用 _run(),那么您的另一个 setTimeout() 将有机会运行。而且,由于那里根本不需要使用 Promise,因此您可以删除它们:

    改成这样:

    class App {
      constructor() {
        this.running = false
        this.cycles = 0
      }
    
      start() {
        this.running = true
        this._run()
      }
    
      stop() {
        this.running = false
      }
    
      _run() {
          this.cycles++
          if (this.running) {
             // call recursively after a short timeout to allow other code
             // a chance to run
             setTimeout(this._run.bind(this), 0);
          }
      }
    }
    
    module.exports = App
    

    有关.then()setImmediate()nextTick() 之间相对优先级的一些讨论,请参阅此其他答案:

    Promise.resolve().then vs setImmediate vs nextTick

    关于这个主题的更多信息:

    https://github.com/nodejs/node-v0.x-archive/pull/8325


    广义的优先级层次似乎是:

    .then()
    nextTick()
    other events already in the queue
    setImmediate()
    setTimeout()
    

    因此,您可以从这里看到 .then() 跳到已经在队列中的其他事件的前面,因此您的 setTimeout() 永远不会运行,只要它们是等待去的 .then()

    因此,如果您希望在您下次调用this._run() 之前允许队列中的其他计时器事件运行,您必须使用setImmediate()setTimeout()。在这种情况下可能两者都可以工作,但由于其他事件是 setTimeout(),我认为在这里使用 setTimeout() 可以保证安全,因为您知道新的 setTimeout() 回调不能跳到已经挂起的回调前面事件。

    【讨论】:

      【解决方案3】:

      您的_run 方法运行速度过快。它使用承诺并且是异步的,因此您不会遇到堆栈溢出或其他问题,但是承诺任务队列的运行优先级高于超时任务队列。这就是为什么你的setTimeout 回调永远不会被触发。

      如果您使用实际长时间运行的任务而不是 Promise.resolve(),它将起作用。

      class App {
        constructor() {
          this.running = false
          this.cycles = 0
        }
      
        start() {
          if (this.running) {
            return Promise.reject(new Error("Already running"))
          } else {
            this.running = true
            return this._run()
          }
        }
      
        stop() {
          this.running = false
        }
      
        _run() {
          return new Promise(resolve => {
            this.cycles++
            setTimeout(resolve, 5) // or something
          }).then(() => {
            if (this.running)
              return this._run()
            else
              return this.cycles
          })
        }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-07-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-03-01
        • 2013-10-30
        • 1970-01-01
        相关资源
        最近更新 更多