问题
I wrote some code below that uses promises and the easiest way I could find to write it was using a Deferred
object instead of the usual Promise executor function because I need to resolve the promise from outside the executor. I'm wondering if there's an accepted design pattern based on the Promise executor function for a problem like this that doesn't use a deferred-like solution? Can it be done without having to resolve the promise from outside the promise executor?
Here are the details.
I have a project that uses a set of Worker Threads and various parts of the code that want to use a Worker Thread from time to time. To manage that, I've created a simple WorkerList
class that keeps a list of the available Worker Threads. When someone wants to use one, they call get()
on it and that returns a promise that resolves to a Worker Thread. If a worker thread is available immediately, the promise resolves immediately. If all worker threads are in use (and thus the list of available workers is empty), then the promise doesn't resolve until one is later put back into the available list via the add(worker)
method.
This WorkerList class has only two methods, add(worker)
and get()
. You get()
a worker and when you're done with it, you add(worker)
it back. When you add(worker)
it back, the class checks to see if there are any tasks waiting for an available Worker. If there, are, it resolves their promise with an available Worker. That resolving of someone else's promise is where the Deferred was used.
Here's the code for the 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;
}
}
}
And, here's the Deferred implementation:
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);
}
}
回答1:
Maybe the below is just a poor man's approach to deferreds, and doesn't really get to the crux of the matter, but instead of a queue of deferreds, you could just keep a queue of resolver functions.
This saves a small amount of code over your approach and avoids explicitly using Deferreds.
I don't know if there is an established pattern for this, but this in itself seems like a reusable pattern for maintaining an asynchronous pool of objects, so rather than calling it WorkerList
, you could name it AsyncPool
, and then compose that as a reusable piece within your 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:
Here is one solution that doesn't expose the promise resolver function anywhere outside the promise executor function.
Following up on my comment to my own question about an event-based solution, here's what I came up with. It uses a triggered event and an event listener to cause an action inside the promise executor function.
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);
});
}
}
}
I was initially concerned about maintaining FIFO ordering so that the first one to call get()
gets the next worker available. But, because eventListeners are called in the order they were added, I think this would actually achieve FIFO order. If there are multiple calls to get()
, they will all get notified about the workerAdded
, but after the first one handles the message and takes the worker, the others will just find no worker left for them so their listener will stay attached waiting for a future workerAdded
message when there is a worker for them (when their listener gets to be first in line).
I don't think I necessarily like this better than the other options shown, but it is an alternative and doesn't use Deferreds or even expose the resolve
handler outside the executor function.
As was suggested, this could also be done where the eventEmitter is an instance variable rather than a base class:
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);
});
}
}
}
来源:https://stackoverflow.com/questions/61358448/can-you-write-this-without-using-a-deferred