JavaScript 코드 병렬화: 병렬화 함수 소개
JavaScript는 단일 스레드 프로그래밍 언어이므로 한 번에 하나의 작업만 실행할 수 있습니다. 이는 이미지 처리 또는 대규모 데이터 분석과 같이 오래 실행되는 무거운 작업을 실행할 때 문제가 될 수 있습니다. 메인 스레드가 정지되고 애플리케이션이 응답하지 않게 될 수 있기 때문입니다.
이 문제를 해결하기 위해 JavaScript 생태계는 스레딩이라는 기술을 개발했습니다. 스레딩을 사용하면 별도의 스레드에서 작업을 실행할 수 있으므로 메인 스레드를 차단하지 않고 애플리케이션 성능에 영향을 주지 않습니다.
이 기사에서는 다른 스레드에서 JavaScript 함수를 실행할 수 있는 parallelizer-function이라는 npm 패키지를 소개합니다. 이 패키지는 브라우저와 Node.js 모두에서 작동하는 Worker API를 사용합니다.
병렬화 기능이란 무엇입니까?
parallelizer-function은 Worker API를 사용하여 별도의 스레드에서 JavaScript 함수를 실행할 수 있는 npm 패키지입니다. 브라우저와 NodeJ 모두에서 작동합니다. 실행 환경을 기준으로 Nodejs 내장 라이브러리 “worker_threads”를 사용하거나 브라우저 환경의 경우 기본 window.Worker 클래스를 사용합니다.
설치
병렬 처리기 기능을 설치하려면 npm 또는 yarn을 사용할 수 있습니다.
npm i parallelizer-function --save
yarn add parallelizer-function
패키지에는 두 가지 주요 핵심 부분인 workerPromise 함수와 Pool 클래스가 있습니다. workerPromise 함수를 사용하면 별도의 스레드에서 함수를 실행할 수 있으며 Pool 클래스는 사전 정의된 최대 수의 스레드 또는 작업자로 모든 작업을 실행하는 스레드 풀을 구현합니다. 사용 가능한 작업자가 현재 작업을 완료하면 함수를 실행하는 비동기 대기열을 구현합니다.
workerPromise기능
이 workerPromise함수는 실행할 함수와 해당 함수에 대한 인수 배열이라는 두 개의 인수를 사용하는 간단한 함수입니다. 함수의 결과로 확인되는 약속을 반환하거나 오류와 함께 거부합니다.
const { workerPromise } = require("parallelizer-function");
const longRunningTask = (n) => {
let result = 0;
for (let i = 0; i < n; i++) {
result += i;
}
return result;
}
async function main() {
// workerPromise(fn: (...params: any[]) => any, args: any[] = []): Promise<any>
workerPromise(longRunningTask,[1_000_000])
.then((res)=>{console.log(res)})
.catch((error)=>{console.error(error)})
/// or using try-catch
try{
let res = await workerPromise(longRunningTask,[1_000_000]);
console.log(res);
}catch(error){
console.error(error)
}
}
Pool클래스: 동시 스레드 수 제한
이 Pool클래스는 애플리케이션이 시스템에 과부하를 주지 않도록 동시 스레드 수를 제한하는 방법을 제공합니다. 이는 이미지 처리와 같은 장기 실행 작업을 실행하고 많은 수의 작업을 동시에 실행하는 데 유용합니다.
const { Pool, pool } = require('parallelizer-function');
// It you want to maintain the global state of the pool use the object pool, but you can define a new Object: const newPool = new Pool(2)
pool.setMaxWorkers(2); // Set the maximum number of worker threads for the global object pool
const longRunningTask = (n) => {
let result = 0;
for (let i = 0; i < n; i++) {
result += i;
}
return result;
};
const heavyImageProcessing = (imageData: ImageData) => {
for (let i = 0; i < imageData.data.length; i += 4) {
// This is a heavy operation that will block the main thread
imageData.data[i] = imageData.data[i] * 2;
imageData.data[i + 1] = imageData.data[i + 1] * 2;
imageData.data[i + 2] = imageData.data[i + 2] * 2;
}
return imageData;
};
async function main() {
try {
//pool.exec(fn: (...params: any[]) => any, args: any[]): Promise<any>
let res = await pool.exec(longRunningTask, [1_000_000]);
console.log(res);
} catch (error) {
console.error(error);
}
/// You can use Promise.all to run several tasks
try {
Promise.all([
pool.exec(longRunningTask, [1_000_000]),
pool.exec(longRunningTask, [2_000_000]),
pool.exec(heavyImageProcessing, [imageData]),
]).then((res) => {
console.log(res);
});
console.log(
'This function will be executed very fast, the above heavy tasks will not block the main thread'
);
} catch (error) {
console.error(error);
}
}
다음으로 할 일은 메서드를 pool사용하여 전역 개체에 대한 최대 작업자 스레드 수를 설정하는 것입니다 setMaxWorkers(2). 즉, 2개의 작업자 스레드만 동시에 실행할 수 있습니다.
그런 다음 장기 실행 작업을 시뮬레이트하는 두 개의 함수 longRunningTask및 를 정의합니다 . heavyImageProcessing이 longRunningTask함수는 단순히 주어진 반복 횟수에 대한 계산을 수행하는 반면 함수 heavyImageProcessing는 이미지에 대해 많은 작업을 수행합니다.
함수 에서 인수로 함수를 실행하는 메서드를 main사용하고 있습니다 .pool.exec()longRunningTask[1_000_000]
제한 사항
- 작업자 함수는 문자열화 또는 복제될 수 있어야 합니다(예: 클래스 메서드일 수 없음).
- 함수가 작업을 수행하는 데 사용하는 모든 라이브러리 또는 패키지는 함수 내에서 가져와야 합니다. 이는 워커가 현재 창과 다른 또 다른 전역 컨텍스트에서 실행되기 때문입니다. 함수는 별도의 스크립트에 있는 것처럼 격리됩니다.
- 몇 가지 예외를 제외하고 작업자 스레드 내에서 원하는 코드를 실행할 수 있습니다. 예를 들어 작업자 내부에서 DOM을 직접 조작하거나 창 개체의 일부 기본 메서드 및 속성을 사용할 수 없습니다.
import { pool } from "parallelizer-function";
import path from "path";
//////////// main script
try{
let pathToFile = path.resolve("../docs/sample-name.txt");
let res:Array<string> = await pool.exec(
async (pathFile)=>{
const fs = require('fs');
let files = fs.readFileSync(pathFile, { encoding: 'utf-8' });
return files.split('\n');
}, [pathToFile]);
console.log("Names: ", res)
}catch(error){
console.error(error)
}
다음은 애플리케이션에서 이 패키지를 사용하는 방법에 대한 몇 가지 예입니다.
- 메인 스레드를 차단하지 않도록 별도의 스레드에서 이미지 처리와 같은 장기 실행 작업을 실행합니다.
- 많은 수의 작업을 동시에 실행하지만 시스템이 과부하되지 않도록 스레드 수를 제한합니다.
- 별도의 스레드에서 작업을 실행하고 콜백 함수를 사용하여 결과 검색
import { Pool } from "parallelizer-function";
let pool = new Pool(4); // Here we instanciate another instance of Worker Pool and we set a pool of 4 workers
function isPrimeThisNumber(n){
// This function takes an integer and returns whether it is a prime number or not. Complexity O(n^1/2)
for(let i=2;i*i<=n;i++){
if(n%i == 0) return false;
}
return true
}
function TripleSum(arr=[]){
// This function return all the distinc triplet i,j,k i<j<k,
// where arr[i] + arr[j] + arr[k] sum up to 0. Complexity O(n^2)
let visited = new Set()
let sol = []
arr = arr.sort()
for(let i =0;i<arr.length;i++){
let target = -arr[i]
let isSeen = new Set()
for(let j=i+1;j<arr.length;j++){
if(isSeen.has(target - arr[j])){
let key = `${arr[i]},${arr[j]},${target - arr[j]}`
if(!visited.has(key))
sol.push([arr[i],arr[j],target - arr[j]])
visited.add(key)
}else{
isSeen.add(arr[j])
}
}
}
return sol
}
function simulateLongTask(delayS = 10){
// This function simulate a task that will take <delayS> seconds to finish
let now = Date.now();
let iter = 0;
let MAX_DELAY = delayS * 1000; // 10 seconds 100000 milliseconds
while((Date.now() - now) < MAX_DELAY ){
iter++;
}
return iter;
}
someBTNEl.addEventListener("click",()=>{
console.log(isPrimeThisNumber(352684978))
/// This will block the main thread
})
// Also this
someBTNEl.addEventListener("click",()=>{
let promiseFn = new Promise((resolve)=>{
resolve(isPrimeThisNumber(352684978));
})
promiseFn.then(console.log);
/// This also will block the main thread.
// Wrapping normal code in a Promise does not guarantee that the main thread will not be blocked.
})
someBTNEl.addEventListener("click",async ()=>{
try{
let res = await pool.exec(isPrimeThisNumber,[352684978]);
console.log(res);
// This will not block the main thread of JS, it will run "isPrimeThisNumber"
// in a separate thread using Worker class.
}catch(error){
}
})
// You can do all computation in once
someBTNEl.addEventListener("click",async ()=>{
Promise.all([
pool.exec(isPrimeThisNumber,[352684978]),
pool.exec(simulateLongTask,[10])
]).then((isPrime,sum] )=>{
/// do more stuff
})
})
// Or in an endpoind for the computation of the functions
let functions = {
TripleSum,
isPrimeThisNumber,
simulateLongTask
}
app.post("/compute/:fn",(req,res)=>{
let fn = req.params.fn;
if(!fn || !(fn in functions)){
return res.status(401).json({msg:"Not found the function"});
}
try{
let res = await pool.exec(functions[fn],req.body?.args || []);
// This will not block the main thread of JS, it will run
return res.status(200).json({ error:false,msg:"OK",data:res });
}catch(e){
return res.status(400).json({ error:true,msg:e.message });
}
})
이미지 처리의 예
아래의 잘린 코드에서 함수는 of 유형을 processImage가져 와서 캔버스 요소를 만들고 그 위에 이미지를 그린 다음 캔버스의 를 가져옵니다. 그런 다음 메인 스레드를 차단하는 무거운 작업인 함수를 실행 한 다음 처리된 것을 다시 캔버스에 놓고 캔버스의 데이터 URL을 반환합니다.imageHTMLImageElementImageDataheavyImageProcessingimageDataimageData
함수 는 함수 processImageInWorker와 매우 유사 processImage하지만 함수를 직접 실행하는 대신 함수와 as 인수를 전달하여 메서드를 heavyImageProcessing사용하여 함수를 실행합니다 . 이것은 개체 를 사용하여 별도의 스레드에서 함수를 실행 하고 기본 스레드를 차단하지 않습니다. 그런 다음 처리된 것을 캔버스에 다시 놓고 캔버스의 데이터 URL을 반환합니다.pool.execheavyImageProcessingimageDataheavyImageProcessingpoolimageData
import { pool } from "parallelizer-function";
const heavyImageProcessing = (imageData: ImageData) => {
for (let i = 0; i < imageData.data.length; i += 4) {
// This is a heavy operation that will block the main thread
imageData.data[i] = imageData.data[i] * 2;
imageData.data[i + 1] = imageData.data[i + 1] * 2;
imageData.data[i + 2] = imageData.data[i + 2] * 2;
}
return imageData;
};
const processImage = async (image: HTMLImageElement) => {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// This will block the main thread
const processedImageData = heavyImageProcessing(imageData);
ctx.putImageData(processedImageData, 0, 0);
return canvas.toDataURL();
};
const processImageInWorker = async (image: HTMLImageElement) => {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// This will not block the main thread
const processedImageData = await pool.exec(heavyImageProcessing, [imageData]);
ctx.putImageData(processedImageData, 0, 0);
return canvas.toDataURL();
};
이 예제에서 응용 프로그램의 "/" 경로는 일련의 긴 작업을 계산한 결과를 반환합니다. 쿼리 매개 변수를 전달하지 않으면 usingPool=true실행이 NodeJ의 기본 스레드에서 실행됩니다.
위의 그림 2에서 끝점 "/"에 도달하면 실행을 완료하는 데 걸리는 시간은 mainHeavyTask2,347초입니다. 그러나 이러한 작업은 메인 스레드에서 실행되기 때문에 들어오는 모든 요청은 메인 스레드가 해제될 때까지 응답을 기다리며 차단됩니다.
위의 그림 3에서 끝점 "/"에 도달했지만 이제 4개의 작업자 풀을 사용하여 실행을 완료하는 데 걸리는 시간은 1.5초 mainHeavyTask(이전보다 950ms 적음)입니다. 그러나 이러한 작업은 서로 다른 스레드에서 실행되므로 들어오는 모든 요청은 정상적으로 실행되며 해당 실행은 의 실행에 영향을 받지 않습니다 mainHeavyTask.
반응을 사용하는 또 다른 예
결론
스레드 풀을 사용하면 별도의 스레드에서 함수를 실행할 수 있으므로 기본 스레드를 차단하지 않습니다. 또한 애플리케이션이 시스템에 과부하를 주지 않도록 동시 스레드 수를 제한할 수 있습니다. 이는 이미지 처리와 같은 장기 실행 작업을 실행하고 많은 수의 작업을 동시에 실행하는 데 유용합니다.
메모
이 패키지는 Node.js v14.x v16.x, v18.x 및 Vanilla JavaScript, React 및 Angular를 사용하는 Mozilla 및 Chrome과 같은 브라우저에서 테스트되었습니다.
작가
저자: 호세 알레한드로 콘셉시온 알바레즈
저장소: 병렬화 기능

![연결된 목록이란 무엇입니까? [1 부]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































