6장: 작업 실행

스레드에서 작업 실행

작업과 스레드

  • 작업이란 추상적이면서 명확하게 구분된 독립적인 업무의 단위를 의미한다.

  • 독립성이 갖춰져 있어야 병렬성이 보장되어 스케줄링 및 부하 분산 등으로 유연하게 처리할 수 있다.

  • 한 작업마다 스레드를 생성하게 되는 경우 스레드를 매우 많이 생성하게 될 수 있다. 이렇게 되면 다음과 같은 문제점이 있다.

    • 스레드 생성 및 제거에 대한 자원 소모가 크기 때문에 지연이 발생할 수 있다.

    • 빈번한 스레드 생성 및 제거는 JVM GC에 가해지는 부하를 높이고 CPU 사용을 위한 경쟁을 높이므로 자원을 많이 사용하게 된다. 스레드를 너무 많이 만들게 된다면 OOM 에러가 발생할 수도 있고 과도한 경쟁으로 인해 성능이 매우 저하될 수 있다.

    • 따라서 애플리케이션에서 만들 수 있는 스레드 개수를 제한하여 성능 저하나 오류 발생을 막아야 한다.

  • 작업 등록과 실행을 분리하는 표준적인 방법이다. 표준(Executor 인터페이스)을 준수하면 코드 변경을 최소화하면서 실행 정책을 쉽게 변경할 수 있다.

    • 실행 정책에는 다음 사항들이 포함된다.

      • 작업을 어떤 스레드에서 수행할 지

      • 작업을 어떤 순서로 실행할 지

      • 동시에 몇 개의 작업을 병렬로 수행할 지

      • 최대 몇 개의 작업이 큐에서 대기할 수 있는지

      • 시스템에 부하가 많이 발생하여 작업을 거절해야 할 때 어떻게 동작할 지

      • 작업을 실행하기 전/후로 어떤 동작을 할 지

  • 각 작업을 Runnable로 정의한다.

  • 작업의 생명주기 관리, 통계 값 제공, 모니터링 등의 기능을 제공한다.

  • 프로듀서-컨슈머 패턴에 기반한 구조를 가진다.

스레드 풀

  • 작업 큐에서 실행할 작업을 가져와 처리할 수 있는 스레드를 풀 형태로 관리하는 것을 의미한다.

  • 매번 스레드를 생성할 필요가 없으므로 요청이 들어왔을 때 딜레이가 발생하지 않고 스레드 풀의 크기를 적절히 조절해두면 하드웨어 프로세서가 모두 쉬지 않고 동작하도록 할 수 있다.

종류

  • newFixedThreadPool

    • 처리할 작업이 등록되면 실제 작업할 스레드를 하나씩 생성한다.

    • 제한된 개수 만큼의 스레드만 생성 가능하다.

  • newCachedThreadPool

    • 현재 풀에 갖고 있는 스레드 수가 처리할 작업 수보다 많아 불필요한 경우 이를 종료시킨다.

    • 스레드 개수에 제한을 두지 않는다.

  • newSingleThreadExecutor

    • 작업을 처리하는 스레드를 하나만 둔다.

    • 큐에 등록된 작업을 순차적으로 수행된다.

  • newScheduledThreadPool

    • 일정 시간 후 작업을 수행하거나 주기적으로 수행할 수 있다.

    • Executor.Timer 클래스의 기능과 유사하다.

      • Timer는 절대 시각과 상대 시각을 모두 지원하지만 이 스레드풀은 상대 시각만 지원한다.

      • Timer는 단일 스레드를 생성해 사용하며, TimerTask 수행 중 예외가 발생할 때 이를 처리하지 않기 때문에 스레드가 멈춰버릴 수 있다. 그렇게 되면 모든 TimerTask 수행에 차질이 생기게 된다.

    • 이 스레드풀과 유사한 기능을 DelayQueue 클래스를 통해 사용할 수 있다. Delayed 시각이 만료된 객체만 take 메서드로 꺼낼 수 있다.

동작 주기

  • 스레드 풀은 running, shutting down, terminated 상태를 가질 수 있다.

  • shutting down 상태에서는 새로운 작업을 등록할 수 없고 이전에 등록되어 있던 작업은 끝낸 후 각 스레드를 종료한다.

  • shutdownNow 메서드를 실행하는 경우 현재 진행중인 작업을 취소시키고 대기중인 작업은 실행하지 않고 스레드풀을 종료한다.

  • ThreadPoolExecutor가 shutting down 또는 terminated 상태일 때 작업을 등록하려 하면 Reject Execution Handler를 통해 처리 방식을 정할 수 있다.

  • awaitTermination 메서드를 통해 terminated 상태가 되기를 대기할 수 있다.

작업

  • Executor는 Runnable, Callable 타입을 작업으로 입력받을 수 있다.

  • Executor에서 작업은 created, submitted, started, completed 상태를 거치며, 중간에 취소할 수도 있다.

  • submit 메서드를 통해 작업이 등록되면 Executor는 Future라는 객체를 반환한다. 이를 통해 작업 결과를 확인하거나 작업을 취소할 수 있다.

  • 다양한 형태의 작업을 병렬로 처리하려고 하는 경우, 작업을 일정한 크기로 유지하기 어렵고 모든 스레드가 바쁜 상태로 만들기가 어렵다. 이렇게 되면 전체적인 성능이 저하될 수 있다.

  • 성능상의 이점을 얻으려고 한다면 대량의 동일한 작업을 정의하여 병렬로 처리해야 한다.

CompletionService

  • Executor의 기능과 BlockingQueue의 기능을 하나로 모은 인터페이스

  • 필요한 Callable 작업을 등록해 실행시킬 수 있고, take/poll 메서드를 통해 작업에 대한 Future를 반환받을 수 있다.

  • 여러 작업을 Executor에서 수행하고 있을 때 각 작업에 대한 Future#get() 메서드를 주기적으로 호출하는 대신 BlockingQueue를 통해 대기하여 먼저 작업이 완료된 것에 대한 결과를 확인할 수 있다.

Last updated