Utilisez async / await pour implémenter l'exclusion mutuelle dans JavaScript

Sep 03 2020

J'avais besoin d'exécuter une séquence de tâches asynchrones en JavaScript. Ils sont asynchrones parce que j'ai besoin qu'ils soient non bloquants, mais je veux toujours que la tâche en cours se termine avant de commencer la suivante. L'ordre des tâches n'a pas d'importance, la seule chose importante est qu'elles sont en exclusion mutuelle les unes avec les autres.

Pour cette raison, peut-être à cause de l'arrière-plan C / C ++, j'étais sur le point de mettre en place une file d'attente et un système de verrous. Ensuite, je m'arrête pour penser si le problème pourrait être résolu simplement en utilisant async / await, et je me suis retrouvé avec cette solution:

previousPromise = null;

async function enqueue(task) {
  while (previousPromise) {
    await previousPromise;
  }
  
  previousPromise = executeTask(task);
  await previousPromise;
  previousPromise = null;
}

En pratique, j'utilise la file d'attente interne des abonnés de promesse comme file d'attente de verrouillage, afin d'obtenir une attente non bloquante. Lorsque la promesse se résout, elle réveille toutes les tâches asynchrones † et la première recherche previousPromiseégale à null continuera.

Voici un exemple de codepen .

Y a-t-il un problème dans cette solution? Merci

  • Note latérale 1 : un vrai système de verrouillage n'est évidemment pas nécessaire. JavaScript est monothread, il n'y a donc aucun risque d'être préempté après le moment et avant l' executeTask affectation.

  • Note latérale 2 : les tâches arrivent du serveur de manière asynchrone, je ne peux donc pas toutes les collecter avant de les exécuter.

† Il appelle en fait le premier rappel de la liste des abonnés

Réponses

2 CertainPerformance Sep 03 2020 at 21:33

Cette boucle:

async function enqueue(task) {
  while (previousPromise) {
    await previousPromise;
  }

bloquera (de manière asynchrone) pour toujours, après la première previousPromiseaffectation à. Une expression qui est une promesse (par exemple, le previousPromise) restera toujours une promesse et sera véridique. Vous pouvez utiliser await previousPromisepour extraire sa valeur de résolution dans une expression différente, mais le previousPromisereste une promesse. Pas plus de deux tâches peuvent être effectuées avec cela enqueue.

Une meilleure méthode serait de réaffecter la promesse à chaque enqueueappel, tout en enchaînant sur la promesse précédente avec un .then:

previousPromise = previousPromise.then(() => executeTask(task));

Il sera également plus facile de gérer si le previousPromisereste la même forme (c'est-à-dire d'une promesse), plutôt que d'avoir à le gérer éventuellement null. Initialisez-le à Promise.resolve().

Démo ci-dessous, où une tâche prend 1 seconde: elle démarre 2 tâches immédiatement, puis 2 autres tâches après 1,5 seconde (donc ces 4 ensemble se terminent après 4 secondes), puis une autre après 7 secondes (qui se termine sur la seconde 8).

const executeTask = () => new Promise(resolve => setTimeout(() => {
  console.log('task resolving');
  resolve();
}, 1000));
let previousPromise = Promise.resolve();

function enqueue(task) {
  previousPromise = previousPromise.then(() => executeTask(task));
}

console.log('start');
enqueue();
enqueue();
setTimeout(() => {
  enqueue();
  enqueue();
}, 1500);
setTimeout(() => {
  enqueue();
}, 7000);

Une autre chose à considérer est la suivante: que voulez-vous qu'il se passe lorsque l'une des tâches est rejetée (si jamais cela est possible)? Vous voudrez peut-être ajouter un .catchà la fin de executeTaskpour vous assurer que la tâche suivante peut être lancée, sans que toute la chaîne Promise ne se brise:

previousPromise = previousPromise
  .then(() => executeTask(task))
  .catch((err) => { /* log error? Make sure previousPromise always resolves */ });