简体   繁体   English

Promise 链接竞态条件

[英]Promise Chaining Race-Condition

I'm currently working on a rather simple logic for processing queued ZPL print jobs, which are stored in an array that's then being iterated sending n amount of copies per job to a printer.我目前正在研究一个相当简单的逻辑来处理排队的 ZPL 打印作业,这些作业存储在一个数组中,然后迭代将每个作业的 n 个副本发送到打印机。

I'm reducing the array into a promise chain mixing-in a sub-chain for each job which sends the copies to the printer.我将数组简化为 promise 链,混合在每个作业的子链中,将副本发送到打印机。 The calls to the printer are synchronous (ye I know...) so I wrapped each one of them into a Promise that only resolves when the printer received the copy, thus ensuring sequential processing.对打印机的调用是同步的(我知道...),所以我将它们中的每一个都包装到一个 Promise 中,仅在打印机收到副本时才解析,从而确保顺序处理。

In case of a failed transmission the current promise rejects with a hand-crafted error which is being caught in the main-chain.在传输失败的情况下,当前的 promise 会拒绝并在主链中捕获手工错误。

So far the theory, alas there seems to be a kind of race-condition between the sub-chains.到目前为止,该理论似乎在子链之间存在一种竞争条件。

I tried my best, but I simply don't see it...我尽力了,但我根本看不到它......

Here some simplified code + fiddle, notice how the sub-chains are not running subsequential:这里有一些简化的代码+小提琴,注意子链是如何不运行的:

['job1', 'job2'].reduce((pMain, item, curIndex) => {

    var pSub = Promise.resolve();

    for (let i = 0; i < 2; i++) pSub = pSub.then(() => new Promise((resolve, reject) => setTimeout(reject, 2000)));

    return pMain.then(() => pSub);

}, Promise.resolve())
.then(() => /* print all done */)
.catch( handleError );

jsfiddle with console.logs here jsfiddle 与 console.logs 在这里

Any advice is highly appreciated.任何建议都将受到高度赞赏。 Being stuck at something so trivial is a bummer.被如此琐碎的事情困住是一件很糟糕的事情。

Your pSub chains are all created and run synchronously during the reduce call.您的pSub链都是在reduce调用期间创建并同步运行的。 To become sequential, they need to go inside the then callback:要成为顺序,他们需要 go 在then回调中:

['job1', 'job2'].reduce((pMain, item, curIndex) => {
    return pMain.then(() => {
        var pSub = Promise.resolve();
        for (let i = 0; i < 2; i++)
            pSub = pSub.then(() => new Promise((resolve, reject) => setTimeout(reject, 2000)));
        return pSub;
    });
}, Promise.resolve())

Alternatively build only a single chain across the two loops:或者在两个循环中只构建一个链:

['job1', 'job2'].reduce((promise, item, outerIndex) => {
    return Array.from({length: 2}).reduce((promise, _, innerIndex) => {
        return promise.then(() => new Promise((resolve, reject) => setTimeout(reject, 2000)));
    }, promise);
}, Promise.resolve())

Of course @jfriend is right, for sequential tasks you should just write async / await code:当然@jfriend 是对的,对于顺序任务,您应该只编写async / await代码:

for (const item of ['job1', 'job2']) {
    for (let i = 0; i < 2; i++) {
        await new Promise((resolve, reject) => setTimeout(reject, 2000));
    }
}

You can also easily put a try block on the right level with that solution.您还可以使用该解决方案轻松地将try块放在正确的级别。

So, by now, you already understand what you were doing wrong in your use of .reduce() to serialize promises.所以,到现在为止,你已经明白你在使用.reduce()序列化 Promise 时做错了什么。 In comments, I made several suggestions that you either:在评论中,我提出了几个建议:

  1. Use modern async/await (with transpiling if necessary)使用现代async/await (必要时进行转译)
  2. Use pre-built libraries that are already offering asynchronous iteration使用已经提供异步迭代的预构建库
  3. Write/use some tested utility functions that you can use instead of hand coding a .reduce() loop each time.编写/使用一些经过测试的实用程序函数,您可以使用它们来代替每次手动编写.reduce()循环。

If #1 or #2 are not practical I suggested making your own tested utility functions because the .reduce() method of serialization is easy to get wrong and not always trivial for someone who hasn't already seen the code to know what it's doing whereas an appropriately named utility function that was written and tested once is simpler to use and understand (once the function is written) and obviously it makes reuse practical too.如果 #1 或 #2 不实用,我建议您制作自己测试过的实用程序函数,因为.reduce()序列化方法很容易出错,而且对于那些还没有看过代码知道它在做什么的人来说并不总是微不足道的而一个适当命名的实用程序 function 编写和测试一次更易于使用和理解(一旦编写 function),显然它也使重用变得实用。

For pre-built libraries, both Bluebird and Async have these capabilities (personally, I prefer Bluebird) and have used Bluebird myself on embedded projects (Raspberry Pi) running older versions of JS.对于预构建的库,Bluebird 和 Async 都具有这些功能(我个人更喜欢 Bluebird),并且我自己在运行旧版本 JS 的嵌入式项目(Raspberry Pi)中使用了 Bluebird。


As for the tested utility functions, here are several you can quickly use.至于经过测试的实用功能,这里有几个您可以快速使用的功能。

iterateAsync() is like an asynchronous .forEach() iterateAsync()就像一个异步的.forEach()

mapAsync() is like an asynchronous .map() mapAsync()就像一个异步.map()

reduceAsync() is like an asynchronous .reduce() reduceAsync()就像一个异步的 .reduce .reduce()

All take an array as the first argument and a function that returns a promise as the second argument.都将一个数组作为第一个参数,将一个 function 返回一个 promise 作为第二个参数。 These are ES5 compatible, but do assume Promise is available.这些是 ES5 兼容的,但假设Promise可用。 Here are the three functions:以下是三个功能:

// iterate an array sequentially, calling a function (that returns a promise)
// on each element of the array
// The final resolved value is whatever the last call to fn(item) resolves to
// like an asynchronous .forEach()
function iterateAsync(array, fn) {
    return array.reduce(function(p, item) {
        return p.then(function() {
            return fn(item);
        });
    }, Promise.resolve());
}

// iterate an array sequentially, calling a function (that returns a promise)
// on each element of the array
// The final resolved value is an array of what all the fn(item) calls resolved to
// like an asynchronous .map()
function mapAsync(array, fn) {
    var results = [];
    return array.reduce(function(p, item) {
        return p.then(function() {
            return fn(item).then(function(val) {
                results.push(val);
                return val;
            });
        });
    }, Promise.resolve()).then(function() {
        return results;
    });
}

// iterate an array sequentially, calling a function fn(item, val)
// (that returns a promise) on each element of the array.  Like array.reduce(),
// the next fn(accumulator, item) is passed the previous resolved value and the promise
// that fn() returns should resolve to the value you want passed to the next
// link in the chain
// The final resolved value is whatever the last call to fn(item, val) resolves to
// like an asynchronous .reduce()
function reduceAsync(array, fn, initialVal) {
    return array.reduce(function(p, item) {
        return p.then(function(accumulator) {
            return fn(accumulator, item);
        });
    }, Promise.resolve(initialVal));
}

Please note that all of these are generally simpler with modern Javascript capabilities (particularly async/await ) so these are mostly for when those modern capabilities are not available or transpiling is not practical.请注意,使用现代 Javascript 功能(尤其是async/await ),所有这些通常都更简单,因此这些主要用于当这些现代功能不可用或转译不实用时。


For completeness, I'll add that using .reduce() in this way is probably not idea for iterating very large arrays.为了完整起见,我将补充一点,以这种方式使用.reduce()可能不适用于迭代非常大的 arrays。 That's because what this does is synchronously pre-build a promise chain p.then().then().then().then() with the number of .then() equal to the length of your array.这是因为它的作用是同步预构建一个 promise 链p.then().then().then().then() ,其中.then() () 的数量等于数组的长度。 If your array was very large (tens or hundreds of thousands of elements long), this could take a significant amount of memory to pre-build all those promises and chain them all together.如果您的数组非常大(数万或数十万个元素长),则可能需要大量的 memory 来预先构建所有这些承诺并将它们链接在一起。

For very large arrays in a "limited environment" like you refer to, you may want to iterate more manually like this which doesn't pre-build any large structure and just uses promises one at a time:对于非常大的 arrays 在您所指的“有限环境”中,您可能希望像这样手动迭代更多,它不会预先构建任何大型结构,并且一次只使用一个承诺:

function iterateAsync(list, fn) {
    return new Promise(function(resolve, reject) {
        var index = 0;

        function next(val) {
            if (index < list.length) {
                try {
                    fn(list[index++]).then(next, reject);
                } catch(e) {
                    reject(e);
                }
            } else {
                resolve(val);
            }
        }
        next();
    });
}

I guess there is a lot of ways to achieve this, but personally what I always do is to create an array of functions that return a Promise (a PromiseFactory, you might say).我想有很多方法可以实现这一点,但就我个人而言,我总是做的是创建一个返回 Promise 的函数数组(你可能会说是 PromiseFactory)。

const promiseFactoryA = () => {
  return new Promise(resolve => {
    console.log('PromiseA started...');
    setTimeout(() => {
      console.log('PromiseA resolved after 300ms');
      resolve();
    })
  }, 300);
}

const promiseFactories = [promiseFactoryA, promiseFactoryA];

Then, we can pass the array to this function that will run them sequentially:然后,我们可以将数组传递给这个 function,它将按顺序运行它们:

const runPromiseSequentially = promiseFactories => {
  let result = Promise.resolve();

  promiseFactories.forEach(
    (promiseFactory) => {
      result = result.then(() => promiseFactory());
    },
  );

  return result;
}

runPromiseSequentially(promiseFactories);

Basically what it does is to ask the PromiseFactory to create the Promise when we want the operation to be started.基本上它的作用是在我们希望开始操作时让 PromiseFactory 创建 Promise。

Example REPL示例REPL

Though, if you could use async and await this would be unnecessary.但是,如果您可以使用asyncawait这将是不必要的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM