Javascript,异步,锁?

Jas*_* Yu 0 javascript asynchronous node.js promise

我知道 async 不是并行的,但我现在遇到了一个非常有趣的情况。

async function magic(){
  /* some processing here */
  await async () => await prompt_for_user(); // 1st prompt
  await async () => await prompt_for_user(); // 2nd prompt
}

magic(); // first
magic(); // second
magic(); // third
Run Code Online (Sandbox Code Playgroud)

从上面的程序中,我们可以很容易地预测出所有提示同时弹出。我尝试使用具有以下实现的队列来解决它:

 const Queue = () => {
  let promise;
  return async (f) => {
    while(promise) await promise;
    promise = f();
    const res = await promises;
    promise = undefined;
    return res;
  };
};


const queue = Queue();
async function magic(){
  /* some processing here */
  await queue(async () => await prompt_for_user()); // 1st prompt
  await queue(async () => await prompt_for_user()); // 2nd prompt
}

magic(); // first
magic(); // second
magic(); // third
Run Code Online (Sandbox Code Playgroud)

这会阻止提示一次全部弹出。但是还有第二个问题:

所以当第一个magic()被调用时。first.1向用户显示提示。程序继续,第二个magic()被调用。另一个提示second.1在出现之前等待第一个提示完成。然后程序继续,第三个magic()被调用并third.1再次等待first.1完成。当first.1完成后,即用户键入的值,second.1就会弹出第一,但我想first.2先弹出。

我知道一个明显的解决方案是一一等待魔法。但这会失去 js 给我们的异步优势。如果在提示前处理魔法重,则提示前需要一些时间。

主意?

jfr*_*d00 7

由于在您发布信号量答案之前我无法理解您的总体目标,因此我将我试图回答的问题定义为这样。

  1. 您希望以尽可能多的并行度运行一系列异步操作(序列化最少的必要数量)。
  2. 一项或多项操作可能需要提示用户输入信息。
  3. 所有用户提示都必须以精确的顺​​序排列代码的排序方式。所以,第一个被调用并提示的函数必须先提示。
  4. 如果同一个函数提示不止一次,它的所有提示都必须出现在任何其他提示之前。
  5. 所以基本上,所有的提示都必须被序列化,但是在提示之前或之后出现的任何其他异步内容都可以并行运行。
  6. 最好的代码不会“旋转”或“轮询”一个标志,而是通过在等待的东西准备好运行时得到通知来为其他操作保留 CPU 周期(承诺背后的一个重要原则)。

这是一个方案,以您发布的方案为蓝本,但它使用一系列承诺(不旋转或轮询标志)来强制prompt()调用序列化(调用之间的任何内容.start().end()同时允许所有其他操作并行运行。这应该是CPU 使用效率更高。

let Semaphore = (function() {
    // private data shared among all instances
    let sharedPromise = Promise.resolve();
    
    return class Sempaphore {
        constructor() {
            let priorP = sharedPromise;
            let resolver;
            
            // create our promise (to be resolved later)
            let newP = new Promise(resolve => {
                resolver = resolve;
            });
            
            // chain our position onto the sharedPromise to force serialization
            // of semaphores based on when the constructor is called
            sharedPromise = sharedPromise.then(() => {
                return newP;
            });
            
            // allow caller to wait on prior promise for its turn in the chain
            this.start = function() {
                return priorP;
            }
            
            // finish our promise to enable next caller in the chain to get notified
            this.end = function() {
                resolver();
            }
        }
    }
})();

// use random times to test our async better
function prompt(tag, n) {
  log(tag, 'input please: ', n);
  return new Promise((resolve) => {
    setTimeout(resolve, Math.floor(Math.random() * 1000) + 500);
  });
};

function log(...args) {
    if (!log.start) {
        log.start = Date.now();
    }
    let diff = ((Date.now() - log.start) / 1000).toFixed(3);
    console.log(diff + ": ", ...args);
}

function randomDelay(low = 500, high = 1000) {
  return new Promise((resolve) => {
    setTimeout(resolve, Math.floor(Math.random() * (high - low)) + low);
  });
}

async function magic1(tag){
  // declare semaphore before any async code to reserve your order for semaphored code below
  let s = new Semaphore();

  // whatever sync or async code you want here
  log(tag, 'do some busy async work 1a');
  await randomDelay(800, 1200);
  log(tag, 'do some busy work 1b');

  // start of our serialized critical section
  await s.start();
  await prompt(tag, 1);
  await prompt(tag, 2);
  s.end();
  // end of our serialized critical section

  // whatever sync or async code you want here
  log(tag, 'do more busy work 1c');
  await randomDelay();
}

async function magic2(tag){
  let s = new Semaphore();
  log(tag, 'do some busy async work 2a');
  // this delay purposely causes magic2 async delay to be shorter 
  // than magic1 for testing purposes
  await randomDelay(100, 750);
  log(tag, 'do some busy work 2b');
  await s.start();
  await prompt(tag, 3);
  await prompt(tag, 4);
  s.end();
  log(tag, 'do more busy work 2c');
  await randomDelay();
}

Promise.all([
    magic1("magic1a"),
    magic1("magic1b"),
    magic2("magic2a"),
    magic2("magic2b")
]).then(() => {
    log("all done");
}).catch(err => {
    log("err: ", err);
});
Run Code Online (Sandbox Code Playgroud)

这是一些示例输出(由于出于测试目的而进行的随机异步延迟,输出会略有不同)。但是,输入调用将始终以完全相同的顺序:

0.000:  magic1a do some busy async work 1a
0.003:  magic1b do some busy async work 1a
0.004:  magic2a do some busy async work 2a
0.004:  magic2b do some busy async work 2a
0.600:  magic2b do some busy work 2b
0.721:  magic2a do some busy work 2b
0.829:  magic1b do some busy work 1b
1.060:  magic1a do some busy work 1b
1.061:  magic1a input please:  1
2.179:  magic1a input please:  2
2.860:  magic1a do more busy work 1c
2.862:  magic1b input please:  1
3.738:  magic1b input please:  2
4.500:  magic1b do more busy work 1c
4.502:  magic2a input please:  3
5.845:  magic2a input please:  4
6.497:  magic2a do more busy work 2c
6.498:  magic2b input please:  3
7.516:  magic2b input please:  4
8.136:  magic2b do more busy work 2c
9.097:  all done
Run Code Online (Sandbox Code Playgroud)

一些解释:

  1. let s = new Sempaphore();在代码中的位置就是你这个函数“把自己放在行中”以进行序列化的地方,所以一些还没有把自己放在行中的东西让它的临界区被迫在这个函数的临界区之后。这“保留”了一个位置,但实际上还没有开始临界区。如果您有其他不确定的异步代码在临界区之前运行,这一点很重要。您需要在异步代码之前在此之前保留您的位置,但不要实际等待该位置直到临界区之前。

  2. await s.start();在函数中放置的位置就是您希望它实际等待您排队等待临界区的位置。

  3. 您放置的位置s.end()是关键部分的结尾(允许其他关键部分现在也可以在轮到它们时运行)。

  4. 此演示显示了在提示的关键部分之前和之后运行的异步代码。该代码可以与其他事物并行运行。

  5. 非输入相关的异步操作可以在输入提示之间交错,即使在同一个临界区(按设计)。只有输入提示被强制序列化。