【问题标题】:Why do Promise libraries use event loops?为什么 Promise 库使用事件循环?
【发布时间】:2014-06-20 07:16:15
【问题描述】:

考虑以下 JavaScript 代码:

var promise = new Promise();
setTimeout(function() {
    promise.resolve();
}, 10);

function foo() { }
promise.then(foo);

在我见过的 promise 实现中,promise.resolve() 会简单地设置一些属性来指示 promise 已解决,并且 foo() 将在事件循环中稍后被调用,但它看起来像 promise.resolve( ) 将有足够的信息来立即调用任何延迟函数,例如 foo()。

事件循环方法似乎会增加复杂性并降低性能,那么为什么要使用它呢?

虽然我对 Promise 的大部分使用是与 JavaScript 一起使用的,但我提出问题的部分原因是在 C++ 游戏等性能非常密集的情况下实现 Promise,在这种情况下,我想知道是否可以利用承诺没有事件循环的开销。

【问题讨论】:

  • 如果你想立即解决一个承诺,你只需调用promise.resolve(),它会在那个时候调用任何注册的解析处理程序。如果您希望自己的事件循环在承诺解决之前展开,那么您可以使用setTimeout(function() {promise.resolve()}, 10);。但是,这取决于您希望.resolve() 的行为方式。不需要使用setTimeout()
  • 是的,我明白这一点。我想了解的是,使用 setTimeout 时,10 毫秒后 foo() 不会立即被调用,而是将分辨率放在队列中,而 foo() 会在一段时间后执行。
  • 哪个承诺实现可以做到这一点?有许多不同的实现,所以很难在不参考具体实现的情况下提出这样的问题。据我所知,没有规范说在延迟之后才应该调用解析回调。
  • 我很想知道哪些承诺实现不这样做。 Promise/A+ spec 中建议使用延迟事件循环,这似乎是假定的方法。如果你需要一个例子,我最熟悉when.js,它使用了这样一个循环。
  • 所以,您问为什么 Promises/A+ 规范的第 2.2.4 节规定:Fulfilled or onRejected must not be called until the execution context stack contains only platform code. [[3.1](#notes)].?我们可以猜测,但您需要一个实际参与规范制作的人来解释他们当时的推理。仅供参考,这种架构中的一些可能假设了一种垃圾收集语言,在该语言中,在堆栈展开后很容易挂起对事物的引用。这在 C++ 中并不容易,因为对于 async resolve(),内存管理要困难得多。

标签: javascript c++ performance promise


【解决方案1】:

promise.resolve() 是否会同步或异步执行其延续实际上取决于实现。

此外,“事件循环”并不是提供不同“执行上下文”的唯一机制。可能还有其他方式,例如线程或线程池,或者考虑提供调度队列的 GCD(Grand Central Dispatch,调度库)。

The Promises/A+ Spec 明确要求继续(onFulfilled 分别为 onRejected 处理程序)将相对于调用 then 方法的“执行上下文”异步执行.

  1. 在执行上下文堆栈仅包含平台代码之前,不得调用 onFulfilledonRejected。 [3.1]。

在注释下,您可以阅读其实际含义:

这里的“平台代码”是指引擎、环境和承诺实现代码。在实践中,此要求可确保 onFulfilled 和 onRejected 在调用 then 的事件循环之后异步执行,并使用新堆栈。

在这里,每个事件都将在不同的“执行上下文”上执行,即使这是同一个事件循环和同一个“线程”。

由于 Promises/A+ 规范是为 Javascript 环境编写的,因此更通用的规范只要求相对于调用 then 方法的调用者异步执行延续。

这样做有充分的理由!

示例(伪代码):

promise = async_task();
printf("a");
promise.then((int result){
    printf("b");
});
printf("c");

假设处理程序(延续)将在与调用站点相同的线程上执行,执行顺序应该是控制台显示如下:

acb

特别是,当一个承诺已经解决时,一些实现倾向于在相同的执行上下文中“立即”(即同步)调用延续。这显然违反了上述规则。

规则调用延续总是异步的原因是调用站点需要保证then之后的处理程序和代码的相对执行顺序,包括延续在任何情况下的声明。也就是说,无论 promise 是否已经解析,语句的执行顺序必须相同。否则,更复杂的异步系统可能无法可靠运行。

对于具有多个同时执行上下文的其他语言的实现来说,另一个糟糕的设计选择——比如多线程环境(在 JavaScript 中不相关,因为只有一个执行线程),即会调用延续 resolve 函数同步。当异步任务将在稍后的事件循环周期中完成时,这甚至是有问题的,因此延续将确实相对于调用站点异步执行。

但是,当异步任务完成后将调用resolve 函数时,此任务可能会在私有 执行上下文(例如“工作线程”)上执行。这个“工作线程”通常是一个专用并且可能是特殊配置的执行上下文——然后调用resolve。如果 resolve 函数将同步执行延续,延续将在任务的私有执行上下文中运行 - 这通常是不希望的。

【讨论】:

    【解决方案2】:

    Promise 都是关于 cooperative multitasking 的。

    几乎唯一的方法是使用基于消息的调度。

    计时器(通常具有 0 延迟)仅用于将任务/消息发布到消息队列中 - 队列中的下一个任务范例。因此,由小型事件处理程序组成的整个结构可以工作,并且您更频繁地屈服 - 所有这些工作都更加顺利。

    【讨论】:

    • @BenjaminGruenbaum 承诺的解决和拒绝方法本身是异步的。调用者不应期望在调用它们时会被阻塞很长时间。建议立即执行可能会阻塞调用者。
    • @BenjaminGruenbaum,正如我所说,调用者不应被解析/拒绝操作执行阻止。每个任务都有自己的时间框架:调用者代码处理自己的东西,实现代码在单独的执行框架中运行(任务,这里的计时器事件处理程序)。否则很难实现合理的多任务处理。
    【解决方案3】:

    所有的 Promise 实现,至少好的实现是这样的。

    这是因为将同步性混入异步 API 是 releasing Zalgo

    事实上,promise 有时不会立即解决,而延迟有时意味着 API 是一致的。否则,您会按照执行顺序获得未定义的行为。

    function getFromCache(){
          return Promise.resolve(cachedValue || getFromWebAndCache());
    }
    
    getFromCache().then(function(x){
         alert("World");
    });
    alert("Hello");
    

    事实承诺库延迟,意味着上述块的执行顺序得到保证。在像 jQuery 这样的破坏承诺实现中,顺序会根据项目是否从缓存中获取而改变。这很危险。

    具有不确定的执行顺序是非常危险的,并且是常见的错误来源。 Promises/A+ 规范将你带入成功的深渊。

    【讨论】:

    • 对于它的价值,这是我最近研究并看起来不错的 ObjectiveC 的承诺实现github.com/mxcl/PromiseKit 它是 Promises/A+ 投诉
    • PromiseKit 根本不符合 Promise/A+。它没有公共解析器 API,而是需要使用提供的类别来实现某些 Foundation 类(例如 NSURLConnection)的解析器方面(或需要实现它们)。此外,当通过给定类别解决承诺时,将调用 同步 的延续,这将有效地在当前执行上下文中执行处理程序 - 调用站点可能不知道该处理程序。所以事实上,PromiseKit 实际上就是你所说的“破碎的实现”(我同意;))
    • PromiseKit 不再发布 Zalgo。休息一下,我在 4 周前发布了它。
    猜你喜欢
    • 2018-03-04
    • 2020-08-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-21
    • 1970-01-01
    相关资源
    最近更新 更多