请告诉我,我很乐意学习新事物 :)
对于像您这样的一项大任务,我认为前两种方法会有点矫枉过正,尽管如果某些块不相关,您可能可以在多个线程/进程之间拆分数据。
第三个选项可能对 setImmediate 很有趣,但放置回调和维护已完成的范围通常很麻烦。
通过函数生成器和 setImmediate 的组合,我们可以在操作的各个点产生长时间运行的函数,函数在产生时的某个点停止,我们调用 setImmediate 让事件循环处理其他事件。完成后,我们可以再次推进长时间运行的函数,直到另一个屈服点。
我们可以重复这个循环,直到长时间运行的函数完成,或者我们可以拦截一些告诉我们停止长时间运行的函数的事件。
举个例子,希望更清楚。
/* Your UpperBound, you can increase it,
the function with yield will be of course slower
*/
const UPPERBOUND = 10
//Plain bigCalculation loop
function bigCalculation() {
let count = 0;
for (let i = 0; i < UPPERBOUND; i++) {
count += i;
}
return count
}
// Function generator, to yield the execution at some point
function* bigCalculationYielder() {
let count = 0;
for (let i = 0; i < UPPERBOUND; i++) {
count += i;
yield count // the yield to suspend the current loop.
/*
conditonal yielding when count is a modulo of 100,
for better performance or use cases
if(count %100){
yield count
}
*/
}
return count
}
function yieldCalculation() {
const calculationYielder = bigCalculationYielder() // get the generator()
function loop() {
/*
Calling next on the generator progress the function
until the next yield point
*/
const iteration = calculationYielder.next()
console.log(iteration)
// When the iteration is done, we can quit the function
if (iteration.done) {
clearInterval(id) // stopping the setInterval function
return
}
// Shorter way
//setImmediate(()=>{loop()}
setImmediate(() => {
// set a litlle time out to see the log pin from the set interval
return setTimeout(() => loop(), 50)
}) // The set immediate function will make progress on the event loop
}
return loop()
}
// A setInterval to see if we can interleave some events with a calculation loop
const id = setInterval(() => console.log("ping"), 50)
const task = yieldCalculation()
/*You can increase the UPPERBOUND constant and use the classic
bigCalculation function. Until this function end, you won't see the
setInterval ping message
*/
在此示例中,我们尝试将函数的进度与从 setInterval 接收的事件交织在一起。
我们可以调用 generator.next() 来进行计算。
如果计算完成,我们清除setInterval和定时器并返回函数,否则调用setImmediate,nodejs可以处理其他事件并再次回调循环函数完成计算。
这种方法也适用于 Promise,
Promise 比回调好一点。您可以在 Promise 中定义一些工作,一旦它解决(成功结束函数),您可以使用 .then 链接操作以获得承诺的结果并对其进行处理。
Promise 通常与异步操作一起使用,但它非常灵活。
const promise=new Promise((resolve,reject)=>{
let i=100;
setTimeout(()=>{
resolve(i)
return;
},2000); // We wait 2 seconds before resolving the promise
})
console.log(promise) // The promise is pending
promise.then(val=>console.log("finish computation with : ",val))
/* once 2 secondes ellapsed, we obtain the result
which been declared in resolve(), inside the promise
*/
从前面带有函数生成器的示例中,我们可以让我们的 yieldCalculation 函数返回一个承诺。 (我改了名字不好意思)
只有当我们完成大计算时,我们才能解决它并用 then 链接它
const UPPERBOUND = 10
function* bigCalculationYielder() {
let count = 0;
for (let i = 0; i < UPPERBOUND; i++) {
count += i;
yield count
}
return count
}
function yieldHandler() {
const calculationYielder = bigCalculationYielder()
/* this time we return a promise, once it the iteration is done
we will set the value through resolve and return
*/
return new Promise((resolve, reject) => {
function loop() {
const iteration = calculationYielder.next()
console.log(iteration)
if (iteration.done) {
// you are setting the value here
resolve(iteration.value)
return
}
//setImmediate(()=>{loop()})
setImmediate(() => {
return setTimeout(() => { loop() }, 50)
})
}
loop()
})
}
const id = setInterval(() => console.log("ping"), 50)
const task = yieldHandler()
console.log(task)
/* When the computation is finished, task.then will be evaluated
and we can chain other operations
*/
task.then(val=>{console.log("finished promise computation with : ",val); clearInterval(id)})
通过这些示例,我们看到我们可以在多个块中执行“长”操作,并让 nodejs 处理其他事件。
Promise 对于操作链来说更好一些。
对于您的用例,我们缺少两部分:
在最后一个示例中,我将创建一个任务类,以处理函数生成器的运行循环,它还将“拦截”SIGINT 信号。
该示例将创建一个 json 对象,其形式为:{datas:[ {a,b} , {a,b} , {a,b} , {a,b} ,...]}
并将其写入文件
我们走吧 !
"use strict";
// used for class declaration, it is javascript strict mode
const fs = require('fs')
// Depending on how fast is your machine
// you can play with these numbers to get a long task and see cancellation with SIGINT
const UPPERBOUND_TASK=10000
const UPPERBOUND_COMPUTATION=10000
// An async generator
// Not usefull here but can be if you want to fetch data from API or DB
async function* heayvyTask() {
let jsonData = { datas: [] };
let i=0;
while (true) {
if(i==UPPERBOUND_TASK){
break
}
heavyComputation(jsonData)
i++
// We yield after the headyComputation has been process.
// Like that we can fill the data by chunck and prevent from yielding too much
yield jsonData
}
return jsonData
}
// The effective process.
// We populate the jsonData object
function heavyComputation(jsonData) {
for (let i = 0; i < UPPERBOUND_COMPUTATION; i++) {
const data = { a: i, b: i + 1 }
jsonData.datas.push(data)
}
}
// Saving the data to a local file
function saveDataToFile(jsonData) {
console.log(jsonData.datas.length)
console.log("saving data to file")
fs.writeFileSync("test.json", JSON.stringify(jsonData))
console.log("done")
}
class Task {
constructor(process) {
//heayvyTask function
this.process = process
this.cancelTask = false
}
start() {
// We are getting the heayvyTask function generator
const process = this.process()
return new Promise(async (resolve, reject) => {
try {
// Declaration of the loop function
async function loop() {
// Here we are using an async function generator
// So we have to await it
// It can be usefull if you peform async operation in it
// Same as before your are running the function till the next yield point
const val = await process.next()
// If the generator function completed
// We are resolving the promise with jsonData object value
if (val.done) {
console.log("task complete")
resolve(val.value)
return
}
// If the task has been canceled
// this.cancelTask is true and we resolve the promise
// All the data handled by the generator will be pass to the promise.then()
if (this.cancelTask) {
console.log("stopping task")
resolve(val.value)
return
}
// Crazy looping
setImmediate( ()=>{
work()
})
}
// We use bind to pass the this context to another function
// Particulary, we want to access this.cancelTask value
// It is related to "this scope" which can be sometimes a pain
// .bind create an other function
const work=loop.bind(this)
// Effectively starting the task
work()
}
catch (e) {
reject(e)
return
}
})
}
// We want to cancel the task
// Will be effetive on the next iteration
cancel() {
this.cancelTask = true
return
}
}
/* We create a task instance
The heavytask generator has been pass as an attribute of the Task instance
*/
let task = new Task(heayvyTask);
// We are running the task.
// task.start() returns a promise
// When the promise resolves, we save the json object to a file with saveDataToFile function
// This is called when the calculation finish or when the task has been interupted
task.start().then(val => saveDataToFile(val)).catch(e=>console.log(e))
// When SIGINT is called, we are cancelling the task
// We simply set the cancelTask attribute to true
// At the next iteration on the generator, we detect the cancellation and we resolve the promise
process.on('SIGINT',()=>task.cancel())
基本流程是:
任务实例 -> 运行任务 -> SIGINT -> 解决承诺 -> 将数据保存到文件
任务实例 -> 运行任务 -> 生成器函数结束 -> 解决承诺 -> 将数据保存到文件
在所有情况下,一旦承诺得到解决,您的文件将以相同的方式保存。
在性能方面,带有 yelding 的生成器函数当然更慢,但它可以提供某种协作,在单线程事件循环上并发,这很好并且可以利用有趣的用例。
使用 setImmediate 循环似乎没问题。我在堆堆栈溢出方面遇到了一些错误,但我认为这与数组结尾太大有关。
如果字符串结束时太大,我也会遇到一些问题,当将它保存到文件时
对于调用堆栈,以递归方式调用循环,setImmediate 看起来很适合它,但我没有调查太多。
在不进一步了解 doCalculation 函数的情况下,我只能建议将屈服点放在有意义的地方。
如果计算遵循某种交易风格,也许在它结束时。否则,你可以有几个屈服点。
在调用 return 语句之前,循环将进一步推动生成器函数
如果您对使用生成器函数的长时间运行的任务感到好奇,this project 似乎提供了一些不错的 API。我没有玩它,但文档看起来不错。
编写和使用它很有趣,如果它能有所帮助,那就太好了
干杯!