Why do Promise libraries use event loops?

2019-02-05 06:30发布

问题:

Considering the following JavaScript code:

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

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

In the promise implementations I've seen, promise.resolve() would simply set some property to indicate the promise was resolved and foo() would be called later during an event loop, yet it seems like the promise.resolve() would have enough information to immediately call any deferred functions such as foo().

The event loop method seems like it would add complexity and reduce performance, so why is it used?

While most of my use of promises is with JavaScript, part of the reason for my question is in implementing promises in very performance intensive cases like C++ games, in which case I'm wondering if I could utilize some of the benefits of promises without the overhead of an event loop.

回答1:

All promise implementations, at least good ones do that.

This is because mixing synchronicity into an asynchronous API is releasing Zalgo.

The fact promises do not resolve immediately sometimes and defer sometimes means that the API is consistent. Otherwise, you get undefined behavior in the order of execution.

function getFromCache(){
      return Promise.resolve(cachedValue || getFromWebAndCache());
}

getFromCache().then(function(x){
     alert("World");
});
alert("Hello");

The fact promise libraries defer, means that the order of execution of the above block is guaranteed. In broken promise implementations like jQuery, the order changes depending on whether or not the item is fetched from the cache or not. This is dangerous.

Having nondeterministic execution order is very risky and is a common source of bugs. The Promises/A+ specification is throwing you into the pit of success here.



回答2:

Whether or not promise.resolve() will synchronously or asynchronously execute its continuations really depends on the implementation.

Furthermore, the "Event Loop" is not the only mechanism to provide a different "execution context". There may be other means, for example threads or thread pools, or think of GCD (Grand Central Dispatch, dispatch lib), which provides dispatch queues.

The Promises/A+ Spec clearly requires that the continuation (the onFulfilled respectively the onRejected handler) will be asynchronously executed with respect to the "execution context" where the then method is invoked.

  1. onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

Under the Notes you can read what that actually means:

Here "platform code" means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.

Here, each event will get executed on a different "execution context", even though this is the same event loop, and the same "thread".

Since the Promises/A+ specification is written for the Javascript environment, a more general specification would simply require that the continuation will be asynchronously executed with respect to the caller invoking the then method.

There are good reasons to this in that way!

Example (pseudo code):

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

Assuming, the handler (continuation) will execute on the same thread as the call-site, the order of execution should be that the console shows this:

acb

Especially, when a promise is already resolved, some implementations tend to invoke the continuation "immediately" (that is synchronously) on the same execution context. This would clearly violate the rule stated above.

The reason for the rule to invoke the continuation always asynchronously is that a call-site needs to have a guarantee about the relative order of execution of handlers and code following the then including the continuation statement in any scenario. That is, no matter whether a promise is already resolved or not, the order of execution of the statements must be the same. Otherwise, more complex asynchronous systems may not work reliable.

Another bad design choice for implementations in other languages which have multiple simultaneous execution contexts - say a multi-threaded environment (irrelevant in JavaScript, since there is only one thread of execution), is that the continuation will be invoked synchronously with respect to the resolve function. This is even problematic when the asynchronous task will finish in a later event loop cycle and thus the continuation will be indeed executed asynchronously with respect to the call-site.

However, when the resolve function will be invoked by the asynchronous task when it is finished, this task may execute on a private execution context (say the "worker thread"). This "worker thread" usually will be a dedicated and possibly special configured execution context - which then calls resolve. If that resolve function will synchronously execute the continuation, the continuation will run on the private execution context of the task - which is generally not desired.



回答3:

Promises are all about cooperative multitasking.

Pretty much the only method to achieve that is to use message based scheduling.

Timers (usually with 0 delay) are simply used to post the task/message into message queue - yield-to-next-task-in-the-queue paradigm. So the whole formation consisting of small event handlers works and more frequently you yield - more smoothly all this work.