Skip to content

dlwldnjs1009/rolling-the-dice

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 

Repository files navigation

주사위 굴리기 연습

요구사항 사람은 주사위를 굴릴 수 있다. 굴려진 주사위는 1~6 사이 무작위 정수를 반환한다. 주사위는 굴릴 때마다 다른 수가 나와야 한다. 주사위를 굴린 결과를 출력할 수 있다.

기능 주사위 던지기 기능 주사위 다시 던지기 기능 새로운 주사위 던지기 기능

추가 구현해볼 기능 스레드 풀을 이용해서 각 스레드마다 요청이 있을 때마다 스레드에서 주사위를 던지도록 구현해보자 비동기도 활용해보자 CompletableFuture 비동기로 주사위 굴리다 에러나는 경우 exceptionally(에러핸들링)도 고려해야 한다.

최종적으로는

  1. 주사위 굴리기
  2. 비동기로 실행
  3. 실행완료되면 콜백함수로 "주사위 몇번 실행완료" 출력
  4. 주사위 굴린 결과 출력

주사위를 굴린다 사람이 주사위를 굴린다 Person class 사람은 주사위를 굴리는 메소드가 있어야 함 랜덤값 생성시키는 클래스, RandomUtil

Dice class, 주사위 클래스에는 숫자를 반환해주는, 사람에게 숫자를 알려주는 메소드가 있어야 한다.(객체 스스로 일을 한다) Dice class 안에는 1~6까지의 숫자 중 하나를 랜덤하게 반환해주는 메소드가 있어야 함.

위처럼 비동기, 스레드풀, Completable을 선택한 이유: 공부를 하며 비동기 vs 동기, 논블락킹 vs 블락킹이 이해가 제대로 안됐다고 느껴서 이번 기회에 실습해보며 익혀보려고 선택했습니다. 스레드풀 같은 경우는 스프링부트를 공부하며 동작방식에서 queue를 이용해서 비동기로 동작한다는 것을 알게되었고 스레드 관리감독을 알아서 해 컴퓨터 리소스를 효율적으로 사용하게 해준다고 알게되어 사용해보려고 선택했습니다. CompletableFuture는 면접 준비할 때 비동기 공부하면서 실습했던 내용인데 이번기회에 다시 복습하고 실습하려고 선택했습니다.

동기 방식이라면 한사람이 주사위를 던지고 그 결과가 나올때까지 기다리고, 결과를 보고 다시 던지고 하는 방식이라 할 수 있습니다. 비동기 방식이라면 한사람이 주사위를 스레드풀의 최대 스레드 개수만큼 박스에 가지고 있고 주사위를 손에서 던져서 굴리고 다시 박스에서 주사위를 꺼내서 주사위를 던질 수 있다고 상황을 설정했습니다.

1초 동안 Executor에 제출된 작업 수 측정 실험 문서

개요

이 실험은 1초 동안 Executor에 할당(submit)된 주사위 굴리기 작업의 총 횟수를 측정하는 것이다. 작업이 1초 이후에 완료되더라도, 제출 시점에 이미 할당된 작업으로 카운트된다.

실험 목적 및 주의 사항

실험 목적은 1초 동안 Executor에 제출된(할당된) 주사위 굴리기 작업의 총 횟수를 측정하는 것이다.
주의 사항은 다음과 같다.

  • 카운트 대상은 1초 동안 Executor에 할당(submit)된 작업 수이다.
  • 작업이 1초 이후에 완료되더라도 카운트에 포함된다.

실험 환경

실행 환경은 M1 Mac Pro이다.
실험에 사용된 Executor 종류는 다음과 같다.

  • SingleThreadExecutor
  • FixedThreadPool (10 threads)
  • CachedThreadPool

실험 방법

실험은 다음과 같이 진행된다.

  1. 시작 시각을 기록하고, 1초 후의 마감 시각(deadline)을 설정한다.
  2. while 루프를 통해 1초 동안 Person.rollTheDice()를 호출하여 작업을 제출한다.
    작업 제출 시마다 AtomicInteger를 증가시켜 제출된 작업 수를 기록한다.
    제출된 작업은 futures 리스트에 저장하여, 후속에 모든 작업의 완료를 대기할 수 있도록 한다.
  3. CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join()를 사용하여 1초 이후에도 완료되는 모든 작업이 종료될 때까지 대기한다.
  4. 최종적으로 1초 동안 제출된 작업 수(AtomicInteger 값)를 출력한다.

예상 가설

예상 가설은 다음과 같다.

  • SingleThreadExecutor는 작업 제출 속도가 상대적으로 낮아 총 작업 횟수가 적을 것으로 예상된다.
  • FixedThreadPool (10 threads)는 다중 스레드를 사용하므로 SingleThreadExecutor보다 작업 제출 속도가 높아 더 많은 작업이 할당될 것으로 예상된다.
  • CachedThreadPool은 스레드 생성 제한이 없으므로 가장 많은 작업이 할당될 것으로 예상된다.

실험 결과

실험 결과는 다음과 같다.

  • SingleThreadExecutor: 4,220,576회
  • FixedThreadPool (10 threads): 4,187,157회
  • CachedThreadPool: 71,687회

결과 분석 및 결론

실험 결과는 각 Executor가 내부적으로 사용하는 작업 큐의 특성에 따라 제출 속도와 작업 할당 횟수가 크게 달라짐을 보여준다.

3 1 2

SingleThreadExecutor와 FixedThreadPool은 내부적으로 LinkedBlockingQueue를 사용한다.
LinkedBlockingQueue는 기본적으로 용량이 매우 크거나 무제한에 가까워 작업 제출 시 블로킹 없이 빠르게 작업을 수용한다.
따라서 메인 스레드가 1초 동안 약 4백만 건 이상의 작업을 빠르게 제출할 수 있었다.

4

반면, CachedThreadPool은 내부적으로 SynchronousQueue를 사용한다.
SynchronousQueue는 내부 버퍼가 없으므로, 작업 제출 시 즉시 다른 스레드가 작업을 받아야 한다.
만약 워커 스레드가 즉시 작업을 수신하지 않으면 제출하는 스레드가 블로킹되어 작업 제출 속도가 크게 떨어진다.
이로 인해 CachedThreadPool에서는 1초 동안 약 7만 건 정도의 작업만 제출되었다.

예상 가설에서는 CachedThreadPool이 스레드 생성 제한이 없으므로 가장 많은 작업이 할당될 것으로 보았으나, 실제로는 SynchronousQueue의 핸드오프 특성 때문에 제출 시 블로킹이 발생하여 작업 제출 속도가 크게 낮아진 것으로 나타났다.
SingleThreadExecutor와 FixedThreadPool은 모두 LinkedBlockingQueue를 사용하므로, 매우 높은 제출 속도를 보이며 결과가 유사하게 나타났다.

결론적으로, 내부 큐의 특성에 따라 Executor의 작업 제출 속도와 할당 횟수가 크게 달라짐을 확인하였다.
LinkedBlockingQueue를 사용하는 경우 1초 동안 약 4백만 건의 작업이 빠르게 할당되고,
SynchronousQueue를 사용하는 경우 작업 제출 시 즉시 핸드오프가 요구되어 블로킹이 발생, 1초 동안 약 7만 건의 작업만 제출됨을 확인하였다.
따라서 Executor 선택 시 내부 큐의 특성을 고려하는 것이 매우 중요하다.

참고:

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/LinkedBlockingDeque.html https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/SynchronousQueue.html

멀티프로세싱 실험 보고서

이 프로젝트는 멀티프로세싱 환경에서 주사위 굴리기 작업(총 1,000,000회)의 성능을 측정한 실험 결과를 담고 있습니다.
아래 보고서는 가설, 실험, 실험 결과결론 형식으로 정리하였습니다.

가설

가설:

  1. 프로세스 수가 코어 수보다 작은 경우:
    각 프로세스가 자체 코어에서 실행되어 경쟁이 적으므로 성능이 좋을 것으로 예상된다.
    다만, 프로세스 수가 너무 적으면 코어 활용률이 떨어질 수 있다.

  2. 프로세스 수가 코어 수와 동일한 경우 (또는 코어 수보다 +1개인 경우):
    I/O 연산이 많지 않은 현재 주사위 굴리기 작업은 CPU Bound 작업에 해당할 수 있다.
    스레드의 경우, Goetz (2002년과 2006년 발표)에 따르면 CPU 바운드 프로그램에서는
    CPU 코어 수보다 하나를 더한 만큼의 스레드를 생성하는 것이 효율적이라고 권장한다.
    이는 컨텍스트 스위칭과 관련이 있으며, 스레드 수를 코어 수에 맞추면 오버헤드를 줄이고 효율을 높일 수 있다.

  3. 프로세스 수가 코어 수보다 많은 경우:
    Context Switch로 인해 CPU 오버헤드가 증가하므로 성능이 저하될 것으로 예상된다.

실험

목표:
백만 개의 주사위 굴리기 작업을 자식 프로세스에 분산하여 전체 작업 완료에 걸리는 총 소요 시간을 측정한다.

실험 방법:

  1. 케이스 1 (코어 수보다 작은 경우):
    • 프로세스 수를 1, 2, 3, 4, 5, 6, 7, 8, 9개로 설정하고 각 경우의 실행 시간을 측정한다.
  2. 케이스 2 (코어 수와 동일한 경우):
    • 현재 코어 수(예: 10개)와 동일한 프로세스 수로 실행한다.
  3. 케이스 3 (코어 수보다 많은 경우):
    • 프로세스 수를 11, 12, …, 20개로 설정하고 실행 시간을 측정한다.

각 프로세스는 DiceRollWorker 클래스를 통해 주어진 횟수만큼 주사위를 굴리며,
ProcessBuilder를 사용해 자식 프로세스를 생성하여 작업 완료 후 자신의 굴림 횟수와 프로세스 ID를 출력한다.
실험은 기존 멀티스레딩 실험과 달리, 자식 프로세스(멀티프로세싱)를 사용하여 진행된다.

추가 사항:

  • 각 케이스별 실행 시간을 기록하고, 가장 빠른 실행 시간이 어떤 경우인지 확인한다.
  • 모든 실험에서 각 프로세스의 굴림 횟수를 합산하면 항상 1,000,000회가 되어야 한다.
실험 결과

실험 결과:

  • 프로세스 수 1개 (코어보다 작음):

    • [Worker] 프로세스 ID: 61949 - 작업 완료: 1000000 회 주사위 굴림
    • 총 소요 시간: 98 ms
  • 프로세스 수 2개 (코어보다 작음):

    • [Worker] 프로세스 ID: 61950 - 작업 완료: 500000 회 주사위 굴림
    • [Worker] 프로세스 ID: 61951 - 작업 완료: 500000 회 주사위 굴림
    • 총 소요 시간: 96 ms
  • 프로세스 수 3개 (코어보다 작음):

    • [Worker] 프로세스 ID: 61952 - 작업 완료: 333333 회 주사위 굴림
    • [Worker] 프로세스 ID: 61953 - 작업 완료: 333333 회 주사위 굴림
    • [Worker] 프로세스 ID: 61954 - 작업 완료: 333334 회 주사위 굴림
    • 총 소요 시간: 110 ms
  • 프로세스 수 4개 (코어보다 작음):

    • 각 Worker: 250000 회
    • 총 소요 시간: 118 ms
  • 프로세스 수 5개 (코어보다 작음):

    • 각 Worker: 200000 회
    • 총 소요 시간: 132 ms
  • 프로세스 수 6개 (코어보다 작음):

    • 각 Worker: 약 166666 회 (마지막 Worker는 166670 회)
    • 총 소요 시간: 155 ms
  • 프로세스 수 7개 (코어보다 작음):

    • 각 Worker: 약 142857 회 (마지막 Worker는 142858 회)
    • 총 소요 시간: 167 ms
  • 프로세스 수 8개 (코어보다 작음):

    • 각 Worker: 125000 회
    • 총 소요 시간: 187 ms
  • 프로세스 수 9개 (코어보다 작음):

    • 8개의 Worker는 111111 회, 1개의 Worker는 111112 회
    • 총 소요 시간: 199 ms
  • 프로세스 수 10개 (코어와 동일):

    • 각 Worker: 100000 회
    • 총 소요 시간: 228 ms
  • 프로세스 수 11개 (코어보다 많음):

    • 대부분의 Worker: 90909 회, 1개의 Worker: 90910 회
    • 총 소요 시간: 244 ms
  • 프로세스 수 12개:

    • 각 Worker: 약 83333 회 (마지막 Worker는 83337 회)
    • 총 소요 시간: 258 ms
  • 프로세스 수 13개:

    • 각 Worker: 약 76923 회
    • 총 소요 시간: 283 ms
  • 프로세스 수 14개:

    • 각 Worker: 약 71428 회 (마지막 Worker는 71436 회)
    • 총 소요 시간: 308 ms
  • 프로세스 수 15개:

    • 각 Worker: 약 66666 회 (마지막 Worker는 66676 회)
    • 총 소요 시간: 324 ms
  • 프로세스 수 16개:

    • 각 Worker: 62500 회
    • 총 소요 시간: 355 ms
  • 프로세스 수 17개:

    • 각 Worker: 약 58823 회
    • 총 소요 시간: 407 ms
  • 프로세스 수 18개:

    • 각 Worker: 약 55555 회 (마지막 Worker는 55565 회)
    • 총 소요 시간: 397 ms
  • 프로세스 수 19개:

    • 각 Worker: 약 52631 회 (마지막 Worker는 52642 회)
    • 총 소요 시간: 378 ms
  • 프로세스 수 20개:

    • 각 Worker: 50000 회
    • 총 소요 시간: 413 ms

총 작업량 확인:
모든 실험에서 각 프로세스가 출력한 주사위 굴림 횟수를 모두 합산하면 항상 1,000,000회임을 확인할 수 있다.

최종 결론:
백만 개 주사위 굴리기 작업에서 가장 빠른 실행 시간은 프로세스 수가 2개일 때 96 ms로 나타났다.

이는 프로세스 수가 코어 수보다 작은 경우에 각 프로세스가 독립적으로 실행되어 컨텍스트 스위칭 및 자원 경쟁이 줄어들어 성능이 향상되었음을 시사한다.

결론
  • 가설 검증:
    실험 결과, 프로세스 수가 1개보다 많고 2개일 때 가장 빠른 실행 시간이 측정되었으며,
    프로세스 수가 증가할수록 (특히 코어 수를 초과하는 경우) 오버헤드로 인해 소요 시간이 증가하는 경향이 확인되었다.

  • 최적 케이스:
    백만 개 주사위 굴리기 작업에서 최적의 성능은 프로세스 수 2개일 때 달성되었으며,
    이 경우 각 프로세스는 500,000회씩 작업을 수행하여 총 96 ms의 빠른 시간 내에 완료되었다.

결론적으로, 멀티프로세싱 환경에서 CPU 바운드 작업의 경우,
프로세스 수를 과도하게 늘리는 것보다 적절한 수(이 경우 2개)의 프로세스를 사용하는 것이 효율적임을 확인할 수 있었다.

참고: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Runtime.html#availableProcessors()

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages