Stop memory leaks with recursive promises

2019-02-08 17:25发布

问题:

How do I create a recursive chain of JavaScript Promises with the Q library? The following code fails to complete in Chrome:

<html>
    <script src="q.js" type="text/javascript"></script>
    <script type="text/javascript">
        //Don't keep track of a promises stack for debugging
        //Reduces memory usage when recursing promises
        Q.longStackJumpLimit = 0;

        function do_stuff(count) {
            if (count==1000000) {
                return;
            }

            if (count%10000 == 0){
                console.log( count );
            }

            return Q.delay(1).then(function() {
                return do_stuff(count+1);
            });
        }

        do_stuff(0)
        .then(function() {
            console.log("Done");
        });
    </script>
</html>

回答1:

This won't stack overflow because promises break the stack, but it will leak memory. If you run this same code in node.js you'll get an error that reads:

FATAL ERROR: CALL_AND_RETRY_2 Allocation failed - process out of memory

What is happening here is that a really long chain of nested promises is being created, each waiting for the next. What you need to do is find a way to flatten that chain so that there is just one top level promise that gets returned, waiting on the inner most promise that is currently representing some real work.

breaking the chain

The easiest solution is to construct a new promise at the top level and use it to break the recursion:

var Promise = require('promise');

function delay(timeout) {
    return new Promise(function (resolve) {
        setTimeout(resolve, timeout);
    });
}

function do_stuff(count) {
    return new Promise(function (resolve, reject) {
        function doStuffRecursion(count) {
            if (count==1000000) {
                return resolve();
            }

            if (count%10000 == 0){
                console.log( count );
            }

            delay(1).then(function() {
                doStuffRecursion(count+1);
            }).done(null, reject);
        }
        doStuffRecursion(count);
    });
}

do_stuff(0).then(function() {
    console.log("Done");
});

Although this solution is somewhat inelegant, you can be sure it will work in all promise implementations.

then/promise now supports tail recursion

Some promise implementations (for example promise from npm, which you can download as a standalone library from https://www.promisejs.org/) correctly detect this case and collapse the chain of promises into a single promise. This works providing you don't keep a reference to the promise returned by the top level function (i.e. call .then on it immediately, don't keep it around).

Good:

var Promise = require('promise');

function delay(timeout) {
    return new Promise(function (resolve) {
        setTimeout(resolve, timeout);
    });
}

function do_stuff(count) {
    if (count==1000000) {
        return;
    }

    if (count%10000 == 0){
        console.log( count );
    }

    return delay(1).then(function() {
        return do_stuff(count+1);
    });
}

do_stuff(0).then(function() {
    console.log("Done");
});

Bad:

var Promise = require('promise');

function delay(timeout) {
    return new Promise(function (resolve) {
        setTimeout(resolve, timeout);
    });
}

function do_stuff(count) {
    if (count==1000000) {
        return;
    }

    if (count%10000 == 0){
        console.log( count );
    }

    return delay(1).then(function() {
        return do_stuff(count+1);
    });
}

var thisReferenceWillPreventGarbageCollection = do_stuff(0);

thisReferenceWillPreventGarbageCollection.then(function() {
    console.log("Done");
});

Unfortunately, none of the built in promise implementations have this optimisation, and none have any plans to implement it.



回答2:

Below is the most simple implementation of what you're trying to do, if this works then there's a problem with the q library, otherwise there's some deep javascript troubles:

<html>
    <script type="text/javascript">
        function do_stuff(count) {
            if (count==1000000) {
                return done();
            }

            if (count%1000 == 0){
                console.log( count );
            }

            return setTimeout(function() { do_stuff(count+1); }, 0);
        }

        do_stuff(0);

        function done() {
            console.log("Done");
        };
    </script>
</html>