“Simplicity is a valuable virtue that requires effort to achieve and knowledge to appreciate. To complicate matters further, complexity tends to sell better.” — Edsger W. Dijkstra
The so-called "lightweight" solution consists of almost 20,000 lines of code and relies on CoffeeScript and Lua. What if you could replace all of that with just 50 lines of JavaScript?
Imagine having a task
that needs some time to compute a result -
async function task(x) {
// task takes time
await pause(random(5000))
// task computes a result
return x * 10
}
Promise.all([1,2,3,4,5,6,7,8,9,10,11,12].map(task))
.then(console.log, console.error)
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]
This executes all twelve (12) tasks simultaneously. If these were requests sent to a server, some connections might be rejected due to overwhelming simultaneous traffic. By introducing a Pool
of threads, we can manage the flow of parallelized tasks -
// my pool with four threads
const pool = new Pool(4)
async function queuedTask(x) {
// wait for a pool thread
const close = await pool.open()
// execute the task and close the thread upon completion
return task(x).then(close)
}
Promise.all([1,2,3,4,5,6,7,8,9,10,11,12].map(queuedTask))
.then(console.log, console.error)
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]
Functions should be concise and focus on accomplishing one specific goal. This approach simplifies feature development and enhances reusability, enabling the combination of simple features into more sophisticated ones. Earlier, you encountered random
and pause
functions -
const random = x =>
Math.random() * x
const pause = ms =>
new Promise(resolve => setTimeout(resolve, ms))
If you wish to apply throttling
to each task, you can specialize pause
to ensure a minimum runtime duration -
const throttle = (p, ms) =>
Promise.all([ p, pause(ms) ]).then(([ value, _ ]) => value)
async function queuedTask(x) {
const close = await pool.open()
// ensure task runs for at least 3 seconds before releasing the thread
return throttle(task(x), 3000).then(close)
}
Promise.all([1,2,3,4,5,6,7,8,9,10,11,12].map(queuedTask))
.then(console.log, console.error)
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]
We can incorporate console.log
messages to monitor the process efficiency. Additionally, introducing a random pause
at the beginning of each task demonstrates that tasks can queue in any order without affecting the final outcome -
async function queuedTask(x) {
await pause(random(5000))
console.log("in queue", x)
const close = await pool.open()
console.log(" sending", x)
const result = await throttle(task(x), 3000).then(close)
console.log(" received", result)
return result
}
Promise.all([1,2,3,4,5,6,7,8,9,10,11,12].map(queuedTask))
.then(console.log, console.error)
console.log |
thread 1 |
thread 2 |
thread 3 |
thread 4 |
[...]
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]
In the above scenario, our pool
was configured with size=4
, allowing up to four tasks to run concurrently. After witnessing sending
four times, a task must have completed, as evidenced by received
, before the next task commences. While queueing
can occur at any time, the Pool
processes queued tasks using an efficient last-in-first-out (LIFO) sequence while maintaining result order.
Moving forward with our implementation, similar to other functions, we can define thread
in a straightforward manner -
const effect = f => x =>
(f(x), x)
const thread = close =>
[new Promise(r => { close = effect(r) }), close]
function main () {
const [t, close] = thread()
console.log("please wait...")
setTimeout(close, 3000)
return t.then(_ => "some result")
}
main().then(console.log, console.error)
please wait...
(3 seconds later)
some result
You can employ thread
to build more sophisticated features like Pool
-
class Pool {
constructor (size = 4) {
Object.assign(this, { pool: new Set, stack: [], size })
}
open () {
return this.pool.size < this.size
? this.deferNow()
: this.deferStacked()
}
deferNow () {
const [t, close] = thread()
const p = t
.then(_ => this.pool.delete(p))
.then(_ => this.stack.length && this.stack.pop().close())
this.pool.add(p)
return close
}
deferStacked () {
const [t, close] = thread()
this.stack.push({ close })
return t.then(_ => this.deferNow())
}
}
Your program is now complete. In the functional demo below, I condensed the definitions for quick reference. Run the program to observe the results in your browser -
[...]
.as-console-wrapper { min-height: 100%; }
I hope you found the insights about JavaScript engaging! If you enjoyed this, consider expanding Pool
capabilities. Perhaps implement a simplistic timeout
function to ensure tasks finish within a designated timeframe. Or introduce a retry
function that repeats a task if it encounters an error or exceeds the time limit. For another application of Pool
to a different problem, check out this Q&A. If you have any queries, feel free to ask for assistance!