스레드

팝콘·2024년 1월 3일

Java

목록 보기
17/17

ExecutorService

자바에서 제공하는 util.concurrent 패키지의 인터페이스로 병렬 작업 시 여러개의 작업을 효율적으로 처리하기 위해 제공된다.
다시말해 개발자가 작업을 분리하고, 수행하는 작업을 손수 구현한다면 각기 다른 Thread를 생성해서 작업을 처리하고 처리가 완료되면 Thread를 제거하는 작업을 진행해야 하는데 ExecutorService를 이용하면 쉽게 처리가 가능하다는 말이다. Task(작업)만 지정해주면 알아서 Thread Pool을 생성해서 Task를 관리한다.

쓰레드는 크게 작업의 등록과 실행으로 나눠진다. ExecutorService는 작업 등록을 위한 인터페이스지만 상속받은 Executor 인터페이스가 작업 실행에 대한 책임을 갖고 있기 때문에 해당 책임도 동시에 책임진다. 기본적으로 Thread Pool은 ExecutorService를 구현한다.

Task는 Queue로 관리된다. ExecutorService가 생성한 Thread Pool의 Thread보다 Task가 많을 때 Task는 Queue에서 대기한다.

종류

CachedThreadPool

  • Executors.newCachedThreadPool() 메소드로 생성할 수 있다.
  • Thread를 캐싱하는ThreadPool이다. 이때 쓰이는 캐싱의 의미는 일정시간동안 Thread를 검사한다는 뜻이다. 60초 동안 작업이 없으면 Pool에서 제거한다.
  • Thread의 제한 없이 무한으로 생성하고 해당 Thread의 작업이 60초 동아 없을 경우 제거하는 방식이기 때문에 개수가 폭발적으로 증가할 수 있다. 작업이 계속 쌓이는 환경에서는 작업이 소멸되는 것보다 쌓이는 것이 더 많다.

FixedThreadPool

  • Executors.newFixedThreadPool(int n) 메소드로 생성할 수 있다.
  • 파라미터로 전달된 값만큼 Thread를 생성한다. 고정된 갯수를 가진 ThreadPool이다. 보통 해당 ThreadPool을 많이 사용한다.

ScheduledThreadPool

  • Executors.newScheduledThreadPool(int corePoolSize) 메소드로 생성할 수 있다.
  • 일정 시간 간격으로 작업을 수행하는 ThreadPool이다.
  • 특정 시간에 작업을 수행하거나 주기적으로 작업을 실행할 수 있다.

SingleThreadExecutor

  • Executors.newSingleThreadExecutor() 메소드로 생성할 수 있다.
  • 한 개의 Thread로 작업을 처리하며 모든 작업이 같은 Thread에서 순차적으로 실행되도록 보장된다.
  • 작업이 동시에 실행되지 않고 순차적으로 실행되어야 할 때 사용된다.
  • 실행되는 순서는 큐에서 지정할 수 있다.
  • 싱글 스레드로 작업을 실행할 때 고려해야 할 race condition을 알아서 처리해준다.
  • 비정상적으로 Thread가 종료되면 새로운 Thread를 생성해서 나머지 작업을 실행한다.

    race condition

    동시에 실행되는 두 개 이상의 Thread가 한 메소드에 접근할 때 동기화 메커니즘 없이 접근하려고 하는 상황을 말한다.

ForkJoinPool

  • 분리할 수 있는 형태인 큰 업무를 작은 업무로 나누어 배분한 다음 작업이 끝났을 때 취합하는 형태다.
  • 작업의 크기에 따라서 분할(Fork)하고 분할된 작업이 처리되면 합쳐(Join)서 반환한다.
  • Thread는 기본적으로 큐(Queue)에 대기하고 있는 작업을 각각 나눠서 실행한다. 이때 작업의 분배량은 작업마다 제각각이기 때문에 나머지 Thread의 유휴타임이 생기는 문제가 있다. 한 Thread는 일을 하고 있는데 나머지 Thread는 놀고 있는 상황이 생기는 것이다. ForkJoinPool은 이런 문제를 해결하고자 탄생했다.
  • ForkJoinPool에서 Thread는 Thread 내부에 있는 데크(Deque)에 작업이 없으면 다른 Thread의 덱에서 작업을 훔쳐온다. 이런 방법으로 놀고있는 Thread가 없도록 방지한다.

    ForkJoinPool에서 Thread 내부에 양방향 큐인 데크를 사용하는 이유는 큐는 선입선출이기 때문이다. Thread는 다른 Thread의 데크에서 아직 실행되지 않은 작업을 가져와야 하기 때문에 양방향 큐인 데크를 사용한다.

제공되는 메서드

라이프사이클 관리를 위한 기능

Executor의 상태 확인과 작업 종료 등 라이프사이클 관리를 위한 메소드를 제공한다.

  • shoutdown

    • 새로운 작업을 더 이상 받아들이지 않는다.
    • 해당 메소드가 호출되기 전에 제출된 작업들은 실행을 보장해준다.
    • 사용 후 ExecutorService는 재사용할 수 없다.
  • shoutdownNow

    • shoutdown 메소드에 더해 이미 제출된 작업들을 인터럽트한다.
    • 실행을 위해 대기중인 작업목록을 반환한다. (List<Runnable>)

      인터럽트(interrup)

      • 방해하다는 뜻의 영단어로 컴퓨터 분야에서는 CPU가 어떤 프로그램을 처리하고 있는데 당장 처리해야 하는 프로그램이 생겼을 경우 인터럽트 신호를 보내서 먼저 처리해야 하는 프로그램을 처리한 후 인터럽트 된 프로그램을 이어서 한다.
      • CPU를 효율적으로 사용하기 위한 기능이다. 문제 상황을 빠르게 처리할 수 있고 프로그램의 이상 현상을 보다 쉽게 파악할 수 있다.

Thread는 데몬 쓰레드(주 쓰레드를 서포트하기 위해 만들어진 쓰레드로 주 쓰레드가 종료되면 같이 종료된다.)가 아니기 때문에 shoutdown / shoutdownNow을 호출하지 않으면 ExecutorService는 다음 작업까지 계속 대기하게 된다.

또한 해당 작업에 인터럽트 여부에 따른 처리가 정의되어 있지 않으면 작업은 계속 실행된다.

shoutdown / shoutdownNow을 호출 후 ExecutorService는 재사용할 수 없다.

  • isShoutdown

    • Executor의 shoutdown 여부를 반환한다.
  • isTerminated

    • shoutdown 실행 후 모든 작업의 종료 여부를 반환한다.
  • awaitTermination

    • shoutdown 실행 후 지정한 시간 동안 모든 작업이 종료될 때까지 대기한다.
    • 지정한 시간 안에 모든 작업이 종료되었는지 여부를 반환한다.

비동기 작업을 위한 기능

비동기란 요청을 보냈을 때 응답 상태와 상관없이 다음 동작을 진행할 수 있는 것이다.

동시에 여러 작업을 실행할 수 있는 메소드다. 비동기 작업의 진행을 추적할 수 있는 Future 인터페이스를 반환한다.

Future 인터페이스

  • 작업은 지금 당장 사용할 수 있는 스레드가 없으면 실행이 미뤄질 수 있고, 작업 시간이 오래 걸릴 수도 있다. 그래서 바로 반환값을 받지 못하고 미래의 어느 시점에 받을 수 있는데 미래의 완료된 작업의 반환값을 구하기 위해 사용되는 것이 Future 인터페이스다.
  • submit

    • 실행할 작업들을 추가하고, 작업의 상태과 결과를 포함하는 Future를 반환한다.
    • Future의 메소드인 get을 호출하면 성공적으로 작업을 마치고 결과를 얻을 수 있다.
    • 작업 처리 중에 예외가 발생하더라도 Thread가 종료되지 않고 다음 작업에 사용된다.
  • invokeAll

    • 모든 결과가 나올 때까지 대기하는 블로킹 방식의 요청이다.
    • 동시에 주어진 작업을 모두 실행하고, 전부 끝나면 각각의 상태와 결과가 포함된 List<Future>를 반환받는다.
    • 최대 ThreadPool의 크기만큼 동시에 실행시키기 때문에 Thread가 충분하다면 가장 오래 걸리는 작업만큼 시간이 소요된다. 하지만 Thread가 충분하지 않다면 가장 오래 걸리는 작업의 시간 + 작업이 대기하는 시간이 추가된다.
  • invokeAny

    • 가장 빨리 실행된 결과가 나올 때까지 대기하는 브로킹 방식의 요청이다.
    • 동시에 주어진 작업을 모두 실행하고, 가장 빨리 완료된 하나의 결과를 Future로 반환받는다.
    • 가장 빨리 끝난 작업의 결과만 반환하므로 동시에 실행된 결과 중에서 가장 짧게 걸리는 작업만큼 시간이 걸린다. 또한 가장 빠르게 끝나는 작업 외 다른 작업들은 실행이 종료된다.

ExecutorService를 사용하는 이유

ExecutorService를 사용하면 shutdown/shutdownNow 메소드를 사용해 쉽게 스레드를 종료할 수 있다.
하지만 일반적으로 스레드는 메소드 안에서 시작이 되는데 메소드가 그 스레드를 리턴해버리면 해당 스레드의 위치를 잃어버리게 된다. 그렇게 되면 스레드는 어딘가에서 계속 살아있게 되는데 만약 스레드가 맡은 작업이 무한반복문일 경우 스레드를 종료할 방법이 없어진다.


Thread Pool

병렬 작업 처리가 많아지게 되면 Thread가 더 많이 생성되고 이는 메모리 사용량이 계속해서 늘어나게 됨을 의미한다. Thread의 관리를 위해 필요한 것이 바로 Thread Pool이다.

병렬성

병렬성은 크게 데이터 병렬성과 작업 병렬성으로 구분된다.

  • 데이터 병렬성
    • 전체 데이터를 쪼개 서브 데이터로 만들고, 서브 데이터를 병렬 처리하여 작업을 빠르게 수행하는 것을 말한다. 서브 데이터는 멀티 코어의 수만큼 쪼개져 각각의 데이터들을 분리된 Thread에서 병렬 처리한다.
  • 작업 병렬성
    • 서로 다른 작업을 병렬 처리하는 것을 말한다. 대표적인 예로는 웹서버로, 각각의 브라우저에서 요청한 내용을 개별 Thread에서 병렬로 처리한다.

Thread Pool은 작업 처리에 필요한 Thread를 제한된 개수만큼 정해 놓고 작업 큐(Queue)에 들어오는 작업에게 Thread를 할당해준다.

왜 필요한가?

서버는 동시에 여러 사람이 접속할 수 있고 접속한 사람의 요청만큼 Thread를 생성해야 한다. 하지만 자바는 Thread를 운영체제의 자원으로 사용하고 있기 때문에 그랬다가는 운영체제의 자원이 빨리 소진된다. Thread Pool은 Thread를 관리하기 위해 필요하다.

장점

자원 효율성

  • Thread Pool은 미리 정해진 개수의 Thread를 생성하여 관리하기 때문에 Thread 생성과 종료에 따른 오버헤드를 줄일 수 있다. 이로써 불필요한 자원의 소모를 방지할 수 있다.

Q . Thread Pool의 종류 중 하나인 CachedThreadPool은 60초 마다 Thread를 삭제하잖아. 그럼 아까 말했던 장점이 소용 없는 거 아닌가?

CachedThreadPool은 지속적으로 작업이 반복되는 환경에선 효율적이나 간혈적으로 작업이 일어나는 환격에선 반복적인 Thread의 생성과 종료로 인해 오버헤드가 발생할 수 있기 때문에 오히려 효율성이 떨어진다.

응답성 처리량 향상

  • 작업이 발생하면 대기 중에 Thread 하나를 작업에 할당하므로 작업을 병렬로 처리할 수 있다. 그럼으로써 응답성과 처리량이 향상하는 것이다.

작업 제어

  • Thread Pool은 Thread의 개수를 제한한다. 이것은 Thread가 처리하는 작업의 양도 제한한다는 뜻이다. Thread Pool은 작업의 양을 제한하므로써 시스템의 부하를 조절하고 과도한 작업 요청으로 인한 성능 저하를 방지할 수 있다.

Thread 관리

  • Thread Pool을 사용하면 Thread의 생성, 재사용, 종료 등을 관리하기 때문에 안전하게 Thread를 이용할 수 있다.

Spring Boot의 Thread Pool

스프링부트엔 내장된 서블릿 컨테이너인 Tomcat이 존재하고 Tomcat 내부에 Thread Pool이 있다. application.yml 또는 application.properties로 Thread Pool을 설정할 수 있다.

server:
  tomcat:
    threads:
      max: 200 # 생성할 수 있는 thread의 총 개수
      min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
    max-connections: 8192 # 수립가능한 connection의 총 개수
    accept-count: 100 # 작업큐의 사이즈
    connection-timeout: 20000 # timeout 판단 기준 시간, 20초
  port: 8080 # 서버를 띄울 포트번호
  • server.tomcat.threads.max
    • 최대 Thread 개수다. 기본은 200이다.
    • 요청에 비해 너무 많이 만들어두면 놀고있는 Thread가 생기게 되어 비효율적이다. 그렇다고 너무 적게 만들어두면 처리할 수 있는 요청 수가 줄어들어 평균 응답 시간이 길어진다.
  • server.tomcat.threads.min-spare
    • 최초에 생성되는 최소한의 Thread의 개수다.
    • 값을 너무 크게 설정하면 항상 유지하고 있어야할 Thread의 수가 많아지기 때문에 메모리 소비가 심해진다.

  • server.tomcat.accept-count
    • max-connection에 도달했을 때 요청을 대기 시키는 최대 길이다.
    • accept-count를 초과하는 요청이 들어오면 거절하거나 timeout 시킨다.
  • server.tomcat.threads.max-connections
    • Tomcat이 최대로 처리할 수 있는 connection의 개수다.

다른 Thread Pool은 모든 Thread가 작업을 실행 중일 때 요청이 들어오면 작업 큐에 대기시킨다. 하지만 Tomcat의 Thread Pool은 min-spare 이상의 요청이 들어오면 즉시 설정해두었던 max만큼의 Thread를 추가로 생성한다.


JVM 자바 스레드 모델

Multi Threading Model

  • 사용자 수준 스레드(User Level Threads)

    • 사용자 라이브러리를 통해 사용자가 생성한 스레드다. 스레드가 생성된 주소 공간에서 실행되고 관리된다.
  • 커널 수준 스레드(Kernel Level Threads)

    • 커널이 생성한 스레드다. 운영체제에 의해서 실행된다. 사용자 수준 스레드보다 생성 및 관리 속도가 느리다.

스레드가 실행할 때 커널의 CPU가 CPU에 스레드를 스케줄링 해야 한다. 그런데 CPU는 커널의 일부분이기 때문에 사용자 수준 스레드를 모른다. 이러한 이유로 사용자 수준 스레드는 커널 수준 스레드에 매핑되어 실행된다. 아래는 매핑의 종류를 설명한다.

커널

  • 컴퓨터 운영체제의 핵심이 되는 프로그램으로 시스템의 모든 것을 제어한다.
  • 보안
    • 컴퓨터 하드웨어와 프로세스의 보안을 책임진다.
  • 자원 관리
    • 자원을 효율적으로 관리하여 프로그램의 실행을 원활하게 진행한다.
  • 추상화
    • 물리적으로 하나뿐인 하드웨어를 여러 사람이 번갈아 사용할 수 있도록 여러개처럼 보이게 한다.

스케줄링

  • 여러 프로세스가 번갈아 사용하는 자원을 어떤 시점에 어떤 프로세스에게 할당할지 결정하는 기법이다.

다대일 모델(Many-to-One Model)

여러 개의 사용자 수준 스레드를 하나의 커널 수준 스레드에 매핑한다. 사용자 수준 스레드 중 하나라고 블락되면 전체 프로세스가 블락된다.
한 번에 하나의 사용자 수준 스레드만이 커널에 접근할 수 있기 때문에 멀티 코어 시스템에서 병렬 처리가 불가능하다.
하지만 동기화 및 리소스 공유가 쉽기 때문에 시간이 단축된다는 장점이 있다.

java 초기 버전의 스레드 모델인 Green Thread가 다대일 모델이었다. 대부분의 컴퓨터가 멀티 코어인 현재는 사용되지 않는다.

일대일 모델(One-to-One Model)

하나의 사용자 수준 스레드를 하나의 커널 수준 스레드에 매핑한다.
하나의 사용자 수준 스레드가 블락되더라도 다른 스레드는 실행이 되기 때문에 병렬 처리가 가능하다.
사용자 수준 스레드를 생성하기 위해 무조건 커널 수준 스레드를 생성해야 한다는 것이 단점이다. 많은 수의 커널 수준 스레드는 시스템이 부담을 줄 수 있다.

다대다 모델(Many-to-Many Model)

여러 개의 사용자 수준 스레드와 같은 수 혹은 그보다 적은 수의 커널 수준 스레드를 연결한다.
커널 수준 스레드의 수는 4코어 시스템보다 7코어 시스템이 더 많다.
가장 높은 정확도의 동시성을 제공하며 앞서 소개한 두 모델의 단점을 절충한다. 하나의 사용자 수준 스레드가 블락되었을 때 커널 수준 스레드는 다른 사용자 수준 스레드의 실행을 스케쥴할 수 있다.
두 수준 모델이라고도 불리는데 일대일 모델로 동작하는 것도 가능하기 때문이다.

하지만 실제로 구현하기 어렵기 때문에 코어의 수가 늘어난 현재로써는 대부분의 운영체제가 일대일 모델로 동작한다.

Green Thread vs Native Thread

Green Thread

  • 자바의 초기 버전의 스레드 모델이며 다대일 모델로 동작한다. 애플리케이션 수준에서 구현되고 라이브러리와 VM에서 관리되며 개발자가 통제할 수 있기 때문에 user-level 쓰레드라고도 불린다.
  • 동기화 및 자원 공유가 쉽고 실행 시간이 빠르지만 멀티 코어 시스템의 이점을 살릴 수 없어 지금은 사용하지 않는다.
  • 동일한 메모리 안에서 스레드가 관리되기 때문에 동기화 작업이 필요없다. 그러므로 속도가 빠르다. 따라서 오버헤드 또한 적다.
  • 우선순위를 지원하지 않기 때문에 어떤 스레드가 먼저 실행될지 알 수 없다.

Native Thread

  • 현재 자바의 스레드 모델이며 다대다 모델로 동작한다. 운영체제에서 구현되고 커널 공간에서 관리된다.
  • 높은 정확도의 동시성을 제공하고 멀티 코어 시스템을 활용할 수 있지만 동기화와 자원 공유가 복잡하기 때문에 실행 시간이 오래 걸린다.
  • 커널이 각각의 스레드를 개별적으로 관리되어 하나의 스레드가 멈춰도 다른 스레드는 실행된다.
profile
터짐

0개의 댓글