avatar
rolldeep

[NestJS] Promise.all 과 Worker_threads의 차이

Aug 20
·
7 min read

병렬처리는 다른 방법도 있지만 일단 이 포스트에서는 Promise.all과 Worker_threads의 차이를 한번 알아보자잇

일단 이 두개를 무슨 목적으로 쓰는걸까?

  1. 성능 : 여러개의 작업을 동시에 처리해서, 실행 시간을 단축시킬래!

  2. 효율 : 여러개의 코어를 가진 프로세서를 다 활용하고싶어!

  3. 확장성 : 요청이 많아도 잘 대응해보고싶어!

뭐 다른 이유도 많겠지만 당장 떠오르는건 이정도인 것 같네잉

구현 코드부터 볼까! 다들 nest cli는 글로벌로 설치 되어있잖어~

nest new nestjs-concurrency-test --strict

main.ts

import { NestFactory } from '@nestjs/core';

import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

//여긴 뭐 없다잉 

app.controller.ts

import { Controller, Get, Query } from '@nestjs/common';

import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('promise-all')
  async testPromiseAll(@Query('i') iterations: string): Promise<string> {
    const start = Date.now();
    const result = await this.appService.testPromiseAll(parseInt(iterations) || 10000000);
    const end = Date.now();

    return `Promise.all: ${end - start} milliseconds, result: ${result}`;
  }

  @Get('worker-threads')
  async testWorkerThreads(@Query('i') iterations: string): Promise<string> {
    const start = Date.now();
    const result = await this.appService.testWorkerThreads(parseInt(iterations) || 10000000);
    const end = Date.now();

    return `Worker Threads: ${end - start} milliseconds, result: ${result}`;
  }
}

컨트롤러에 엔드포인트 두개를 맛있게 만들었엉 NestJS는 다들 아는거 맞지? 모르면 물어봐잉

쿼리 파라미터 i를 받아서 몇번 반복할건지 처리할거임 ㅋㅋ 파라미터 없으면 천만번 ㅅㄱ링

리턴값은 시간이 얼마나 걸렸는지 보여줄거임. result는 사실 별로 안중요해. 어차피 파라미터가 같다면 둘은 같은 값을 뱉을것

실제 로직은 당연히 서비스코드에 있으니까 별거없지? (사실 서비스코드도 별거없음)

app.service.ts

import { Injectable } from '@nestjs/common';
import path from 'path';
import { Worker } from 'worker_threads';

@Injectable()
export class AppService {
  computation(iterations: number): number {
    return Array.from({ length: iterations }, (_, i) => Math.sqrt(i)).reduce(
      (acc: number, cur: number) => acc + cur,
      0,
    );
  }

  async testPromiseAll(iterations: number): Promise<number> {
    const tasks = Array(4)
      .fill(null)
      .map(() => this.computation(iterations / 4));

    const results = await Promise.all(tasks);

    return results.reduce((acc: number, cur: number) => acc + cur, 0);
  }

  async testWorkerThreads(iterations: number): Promise<number> {
    const workerPath = path.resolve(__dirname, 'workers/worker.js');
    const workers = Array(4)
      .fill(null)
      .map(
        () =>
          new Promise<number>((resolve, reject) => {
            const worker = new Worker(workerPath, { workerData: { iterations: iterations / 4 } });

            worker.on('message', resolve);
            worker.on('error', reject);
          }),
      );

    const results = await Promise.all(workers);

    return results.reduce((acc: number, cur: number) => acc + cur, 0);
  }
}

일단 Promise.all, worker_threads 에서 처리할 computation 함수를 맛있게 만들었당

(처음에 for문으로 했다가 멋있어 보이려고 reduce 썼잔어 ㅋ)

이 함수는 iterations 만큼 배열을 만들고, 제곱근을 계산해서 그 합을 반환할거야

이 코드도 별거없어. 두 함수 다 태스크를 4개로 나눠서 처리한다

다만 두 함수는 각각 다른 방식으로 동시성을 구현하는데,

testPromiseAll 에서는 비동기 특성을 활용해서 동시성을 구현하지만 결국엔 '단일 스레드'에서 실행된다.

testWorkerThreads 에서는 실제로 다중 스레드를 사용해서 병렬적으로 처리한다잉

결과를 한번볼까?

파라미터 i 를 1000000 (백만)으로 주었을때

until-1252until-1253

백만번으로는 사실 별 차이 없다 ㅇㅅㅇ 오히려 promise-all로 구현했을때 아주 조금 더 빨리, 혹은 비슷한 결과가 나옴

근데 i를 10000000 (천만)으로 주었을땐

until-1254until-1255

2배 이상 차이가 난단말이지, 여러번 시도해도 worker-threads가 2배 이상 빠르다!

그럼 이 두 방식 각각의 장단점이 뭔지 알아볼까잇

  1. Promise.all 의 장단점

    • 장점

      • 매우 구현이 간편하다!

      • 메모리를 덜 씀

      • Node.js 환경 말고도 브라우저에서도 쓸수 있겠지?

    • 단점

      • CPU단에서 실제 병렬 처리는 하는것은 아님

      • 하나의 스레드에서 실행되기 때문에 여러 작업중에 하나가 블로킹되면 전체 성능이 떡락할지도?

  2. Worker Threads의 장단점

    • 장점

      • 실제로 다중 코어를 활용함

      • 각각의 Worker가 독립되어서 안정성이 높음

      • 무거운 연산을 할땐 엄청 빠르다!

    • 단점

      • 구현이 좀 상대적으로 귀찮음(사실 쓰기 좀 꺼려지는 가장 큰 원인)

      • 메모리를 많이 씀

      • 오버헤드가 발생할지도..?

      • Node에서 지원하기 때문에 Node가 아니면 못써잉

그럼 또 이제 어디에 쓰면 좋을지 알아봐야겠지

  1. Promise.all 활용처

    • 비동기 API를 한번에 호출 할 때

    • 간단한 병렬 처리만을 필요로 할 때

    • I/O 작업을 동시에 처리할 때

  2. Worker Threads 활용처

    • 엄청 거대한 데이터 처리

    • 엄청 복잡한 연산

    • CPU가 두뇌 풀가동 해야할 때(이미지나 동영상 등 ?)

    • 백그라운드에서 오래오래 실행되어야할 때

결론

알잘딱으로 잘 선택해야한다.

Promise.all 이 편하긴해.. 자매품 Promise.race도 애용하자