thread_pools
Version:
Thread pool with Auto-configuration for worker_threads, provides both thread and pool function, has thread-safe storage
284 lines (250 loc) • 9.88 kB
JavaScript
const { Worker, isMainThread, SHARE_ENV } = require("worker_threads");
const os = require("os");
const serialize = require("serialize-javascript");
let functionSlicer = (func = () => {}) => {
let whole = func.toString();
return whole.slice(whole.indexOf("{") + 1, whole.lastIndexOf("}"));
};
/**
* worker receives type :
*
* [{type:"eval", func, param},{type:"getLock", lock},{type:"getStorage",storage}, {type:"complete", id}]
*
* worker can perform/post type:
*
* ["msg","msg-warn","msg-error","result","reject","getLock","unlock","getStorage","setStorage", "complete"]
*/
let workerLogic = exitAfter => {
const { parentPort } = require("worker_threads");
const assist = {};
const _subProcessing = {};
assist.serialize = require("serialize-javascript");
assist.post = (data, type = "msg") => parentPort.postMessage({ data: assist.serialize(data), type });
assist.sleep = seconds => new Promise(resolve => setTimeout(() => resolve(), seconds * 1000));
_subProcessing.unlocked = false;
_subProcessing.completeid = 0;
_subProcessing.complete = {};
_subProcessing.temp_storage = {};
assist.lock = async () => {
while (!_subProcessing.unlocked) await assist.sleep(0.1).then(() => assist.post("", "getLock"));
};
assist.unlock = async () => {
assist.post("", "unlock");
_subProcessing.unlocked = false;
};
//wait for worker's event queue to reach that point
assist.waitComplete = async callback => {
let id = _subProcessing.completeid++;
return new Promise(resolve => resolve(callback())).then(async data => {
assist.post(id, "complete");
while (!_subProcessing.complete[id]) await assist.sleep(0.1);
delete _subProcessing.complete[id];
return data;
});
};
assist.autoLocker = async callback => {
return assist
.lock()
.then(() => assist.waitComplete(callback))
.then(() => assist.unlock())
.catch(e => assist.unlock().then(() => Promise.reject(e)));
};
assist.storage = async callback => {
await assist.lock();
await assist.waitComplete(() => assist.post("", "getStorage"));
await new Promise(resolve => resolve(callback(_subProcessing.temp_storage))).catch(error => {
assist.unlock();
return Promise.reject(error);
});
await assist.waitComplete(() => assist.post(_subProcessing.temp_storage, "setStorage"));
await assist.unlock();
return _subProcessing.temp_storage;
};
Object.freeze(assist);
console = {
log: (...data) => assist.post(data, "msg"),
warn: (...data) => assist.post(data, "msg-warn"),
error: (...data) => assist.post(data, "msg-error")
};
let evaluate = item => {
let func = eval("(" + item.func + ")");
let result = Promise.resolve(func(...eval("(" + item.param + ")")));
result
.then(data => assist.post(data, "result"))
.catch(error => assist.post(error, "reject"))
.then(() => (exitAfter ? process.exit() : ""));
};
parentPort.on("message", message => {
if (message.type == "eval") return evaluate(message);
if (message.type == "getLock") return (_subProcessing.unlocked = message.lock);
if (message.type == "getStorage") return (_subProcessing.temp_storage = message.storage);
if (message.type == "complete") return (_subProcessing.complete[message.id] = true);
});
};
class Pool {
/**
*
* @param {{threads:Number, importGlobal:string, waitMs:Number}} config threads : CPUNo. < 3 ? 2 : (CPUNo. * 2 - 2)
*
* importGlobal : <require / import> statement, for thread pool environment, reduce overhead
*
* waitMs : the frequency of threadPool checking if thread is avaliable, default: 100
*/
constructor(config = {}) {
let defaultConfig = {
threads: os.cpus().length < 3 ? 2 : os.cpus().length * 2 - 2,
importGlobal: ``,
waitMs: 100
};
config = Object.assign(defaultConfig, config);
this.entry = {};
this.storage = {};
this.entry.threadNo = config.threads;
this.entry.importGlobal = config.importGlobal;
this.entry.waitMs = config.waitMs;
this.entry._lock = false;
this.entry._threaduid = 1;
this.entry._threadCancelable = {}; // {uid : threadid} threadid corresponding to _threadpPools
this.entry._threadPools = {};
this.entry._threadAvailableID = Array(this.entry.threadNo)
.fill()
.map((i, index) => index);
this.entry.workerMaker = (exitAfter = true) =>
new Worker(this.entry.importGlobal + `\nlet exitAfter = ${exitAfter};\n` + functionSlicer(workerLogic), {
eval: true,
env: SHARE_ENV
});
this.entry.getLock = worker => {
let lock = this.entry._lock == false ? (this.entry._lock = true) : false;
worker.postMessage({ lock, type: "getLock" });
};
this.entry.getStorage = worker => {
worker.postMessage({ storage: this.storage, type: "getStorage" });
};
this.entry.setStorage = pairs => (this.storage = pairs);
this.entry.setComplete = (worker, id) => worker.postMessage({ type: "complete", id });
this.entry.unlock = () => (this.entry._lock = false);
this.entry.setListener = (worker, resolve, reject) => {
worker.removeAllListeners();
worker.on("message", message => {
message.data = eval("(" + message.data + ")");
if (message.type == "msg") return console.log(...message.data);
if (message.type == "result") return resolve(message.data);
if (message.type == "reject") return reject(message.data);
if (message.type == "msg-warn") return console.warn(...message.data);
if (message.type == "msg-error") return console.error(...message.data);
if (message.type == "getLock") return this.entry.getLock(worker);
if (message.type == "unlock") return this.entry.unlock();
if (message.type == "getStorage") return this.entry.getStorage(worker);
if (message.type == "setStorage") return this.entry.setStorage(message.data);
if (message.type == "complete") return this.entry.setComplete(worker, message.data);
});
worker.once("error", error => reject(error));
worker.once("exit", () => resolve());
};
this.entry.publisher = (data, uid) => {
let threadid = this.entry._threadCancelable[uid];
this.entry._threadAvailableID.push(threadid);
delete this.entry._threadCancelable[uid];
return data;
};
this.entry.terminatePoolProtocol = uid => {
let threadid = this.entry._threadCancelable[uid];
let thread = this.entry._threadPools[threadid];
if (threadid && thread && thread.terminate) {
thread.terminate();
delete this.entry._threadPools[threadid];
delete this.entry._threadCancelable[uid];
}
};
}
/**
*
* @return {{cancel:Function, result:Promise}}
*/
threadSingleStoppable(func, ...param) {
if (isMainThread) {
let worker = this.entry.workerMaker();
worker.postMessage({ func: serialize(func), param: serialize(param), type: "eval" });
return {
cancel: () => worker.terminate(),
result: new Promise((resolve, reject) => this.entry.setListener(worker, resolve, reject))
};
}
return {
cancel: () => {},
result: Promise.reject("This is not in the main thread")
};
}
/**
* @param func {Function}
*/
async threadSingle(func, ...param) {
return this.threadSingleStoppable(func, ...param).result;
}
async threadPool(func, ...param) {
return this.threadPoolStoppable(func, ...param)
.then(data => data.result)
.catch(e => Promise.reject(e));
}
/**
*
* @param {Number} uid if(uid > 0) delete corresponding uid, the thread won't be stopped if uid is already resolved/rejected
*
* if(uid == 0) ? delete all threads from thread pool
*
* if(uid < 0) ? Nothing;
*/
async _threadPoolStop(uid = 0) {
if (uid > 0) this.entry.terminatePoolProtocol(uid);
if (uid == 0) {
Object.keys(this.entry._threadCancelable).map(uid => this.entry.terminatePoolProtocol(uid));
Object.values(this.entry._threadPools).map(thread => thread.terminate());
this.entry._threadPools = {};
this.entry._threadAvailableID = Array(this.entry.threadNo)
.fill()
.map((i, index) => index);
}
}
/**
* @return {Promise<{result:Promise,uid:Number, cancel:Function}>}
*/
async threadPoolStoppable(func, ...param) {
if (this.entry._threadAvailableID.length <= 0) {
await new Promise(resolve => setTimeout(() => resolve(), this.entry.waitMs));
return this.threadPoolStoppable(func, ...param);
}
if (isMainThread) {
let threadid = this.entry._threadAvailableID.pop();
let uid = this.entry._threaduid++;
this.entry._threadCancelable[uid] = threadid;
if (!this.entry._threadPools[threadid]) this.entry._threadPools[threadid] = this.entry.workerMaker(false);
this.entry._threadPools[threadid].postMessage({ func: serialize(func), param: serialize(param), type: "eval" });
return {
uid,
cancel: () => this.entry.terminatePoolProtocol(uid),
result: new Promise((resolve, reject) =>
this.entry.setListener(this.entry._threadPools[threadid], resolve, reject)
)
.then(data => this.entry.publisher(data, uid))
.catch(error => Promise.reject(this.entry.publisher(error, uid)))
};
}
return {
uid: -1,
cancel: () => {},
result: Promise.reject("This is not in the main thread")
};
}
}
const assist = {
serialize: () => {},
sleep: async seconds => {},
lock: async () => {},
unlock: async () => {},
waitComplete: async callback => {},
autoLocker: async callback => {},
storage: async (callback = (store = {}) => {}) => {}
};
module.exports = Pool;
module.exports.assist = assist;