J'ai écrit un code ci-dessous qui utilise des promesses et le moyen le plus simple que j'ai pu trouver pour l'écrire était d'utiliser un objet Deferred au lieu de la fonction habituelle de l'exécuteur Promise parce que je dois résoudre la promesse de l'extérieur de l'exécuteur. Je me demande s'il existe un modèle de conception accepté basé sur la fonction d'exécuteur Promise pour un problème comme celui-ci qui n'utilise pas de solution de type différé? Cela peut-il être fait sans avoir à résoudre la promesse de l'extérieur de l'exécuteur testamentaire?

Voici les détails.

J'ai un projet qui utilise un ensemble de threads de travail et diverses parties du code qui souhaitent utiliser un thread de travail de temps en temps. Pour gérer cela, j'ai créé une classe WorkerList simple qui conserve une liste des threads de travail disponibles. Quand quelqu'un veut en utiliser un, il appelle get() dessus et cela renvoie une promesse qui se résout en un thread de travail. Si un thread de travail est disponible immédiatement, la promesse se résout immédiatement. Si tous les threads de travail sont en cours d'utilisation (et que la liste des workers disponibles est donc vide), alors la promesse ne se résout pas tant que l'un n'est pas remis dans la liste disponible via la méthode add(worker).

Cette classe WorkerList n'a que deux méthodes, add(worker) et get(). Vous get() êtes un travailleur et lorsque vous en avez terminé, vous add(worker) le récupérez. Lorsque vous le add(worker) revenez, la classe vérifie si des tâches attendent un Worker disponible. S'il y en a, cela résout leur promesse avec un ouvrier disponible. Cette résolution de la promesse de quelqu'un d'autre est l'endroit où le différé a été utilisé.

Voici le code du WorkerList:

class WorkerList {
    constructor() {
        this.workers = [];
        this.deferredQueue = [];
    }
    add(worker) {
        this.workers.push(worker);

        // if someone is waiting for a worker,
        // pull the oldest worker out of the list and
        // give it to the oldest deferred that is waiting
        while (this.deferredQueue.length && this.workers.length) {
            let d = this.deferredQueue.shift();
            d.resolve(this.workers.shift());
        }
    }
    // if there's a worker, get one immediately
    // if not, return a promise that resolves with a worker
    //    when next one is available
    get() {
        if (this.workers.length) {
            return Promise.resolve(this.workers.shift());
        } else {
            let d = new Deferred();
            this.deferredQueue.push(d);
            return d.promise;
        }
    }
}

Et voici l'implémentation différée:

function Deferred() {
    if (!(this instanceof Deferred)) {
        return new Deferred();
    }
    const p = this.promise = new Promise((resolve, reject) => {
        this.resolve = resolve;
        this.reject = reject;
    });
    this.then = p.then.bind(p);
    this.catch = p.catch.bind(p);
    if (p.finally) {
        this.finally = p.finally.bind(p);
    }
}
2
jfriend00 22 avril 2020 à 09:15

2 réponses

Meilleure réponse

Peut-être que ce qui suit n'est qu'une approche pauvre des différés, et ne va pas vraiment au coeur du problème, mais au lieu d'une file d'attente de différés, vous pouvez simplement garder une file de fonctions de résolution.

Cela économise une petite quantité de code sur votre approche et évite d'utiliser explicitement les différés.

Je ne sais pas s'il existe un modèle établi pour cela, mais cela en soi semble être un modèle réutilisable pour maintenir un pool d'objets asynchrone, donc plutôt que de l'appeler WorkerList, vous pouvez le nommer {{X1} }, puis composez-le comme un élément réutilisable dans votre WorkerList:

class AsyncPool {
    constructor() {
        this.entries = [];
        this.resolverQueue = [];
    }
    add(entry) {
        console.log(`adding ${entry}`);
        this.entries.push(entry);

        // if someone is waiting for an entry,
        // pull the oldest one out of the list and
        // give it to the oldest resolver that is waiting
        while (this.resolverQueue.length && this.entries .length) {
            let r = this.resolverQueue.shift();
            r(this.entries.shift());
        }
    }
    // if there's an entry, get one immediately
    // if not, return a promise that resolves with an entry
    //    when next one is available
    get() {
        return new Promise((r) => 
            this.entries.length
                ? r(this.entries.shift())
                : this.resolverQueue.push(r)
        );
    }
}


let pool = new AsyncPool();

pool.add('Doc');
pool.add('Grumpy');
pool.get().then(console.log);
pool.get().then(console.log);
pool.get().then(console.log);
pool.get().then(console.log);

// add more entries later
setTimeout(() => pool.add('Sneezy'), 1000);
setTimeout(() => pool.add('Sleepy'), 2000);
2
JLRishe 22 avril 2020 à 07:56

Voici une solution qui n'expose la fonction de résolution de promesse nulle part en dehors de la fonction d'exécuteur de promesse.

Suite à mon commentaire sur ma propre question sur une solution événementielle, voici ce que j'ai proposé. Il utilise un événement déclenché et un écouteur d'événements pour provoquer une action dans la fonction exécuteur de promesse.

class WorkerList extends EventEmitter {
    constructor() {
        this.workers = [];
    }
    add(worker) {
        this.workers.push(worker);
        // notify listeners that there's a new worker in town
        this.emit('workerAdded');
    }
    // if there's a worker, get one immediately
    // if not, return a promise that resolves with a worker
    //    when next one is available
    get() {
        if (this.workers.length) {
            return Promise.resolve(this.workers.shift());
        } else {
            return new Promise(resolve => {
                const onAdded = () => {
                    if (this.workers.length) {
                        this.off('workerAdded', onAdded);
                        resolve(this.workers.shift());
                    }
                }

                this.on('workerAdded', onAdded);
            });
        }
    }
}

J'étais initialement préoccupé par le maintien de l'ordre FIFO afin que le premier à appeler get() obtienne le prochain worker disponible. Mais, comme les eventListeners sont appelés dans l'ordre dans lequel ils ont été ajoutés, je pense que cela permettrait en fait d'obtenir un ordre FIFO. S'il y a plusieurs appels à get(), ils seront tous informés du workerAdded, mais une fois que le premier aura traité le message et pris le worker, les autres ne trouveront plus de worker pour eux, donc leur l'auditeur restera attaché en attendant un futur message workerAdded lorsqu'il y aura un worker pour lui (lorsque son auditeur sera le premier en ligne).

Je ne pense pas que j'aime nécessairement cela mieux que les autres options présentées, mais c'est une alternative et n'utilise pas Deferreds ni même expose le gestionnaire resolve en dehors de la fonction exécuteur.


Comme cela a été suggéré, cela peut également être fait lorsque eventEmitter est une variable d'instance plutôt qu'une classe de base:

class WorkerList {
    constructor() {
        this.workers = [];
        this.emitter = new EventEmitter();
    }
    add(worker) {
        this.workers.push(worker);
        // notify listeners that there's a new worker in town
        this.emitter.emit('workerAdded');
    }
    // if there's a worker, get one immediately
    // if not, return a promise that resolves with a worker
    //    when next one is available
    get() {
        if (this.workers.length) {
            return Promise.resolve(this.workers.shift());
        } else {
            return new Promise(resolve => {
                const onAdded = () => {
                    if (this.workers.length) {
                        this.emitter.off('workerAdded', onAdded);
                        resolve(this.workers.shift());
                    }
                }

                this.emitter.on('workerAdded', onAdded);
            });
        }
    }
}
2
jfriend00 22 avril 2020 à 10:22