ExecutorService의 종료 메서드
ExecutorService를 줄여서 앞으로 서비스라고 하겠다. ExecutorService 에는 종료와 관련된 다양한 메서드가 존재한다.
서비스 종료
- void shutdown()
- 새로운 작업을 받지 않고, 이미 제출된 작업을 모두 완료한 후에 종료한다.
- 논 블로킹 메서드(이 메서드를 호출한 스레드는 대기하지 않고 즉시 다음 코드를 호출한다.)
- List<Runnable> shutdownNow()
- 실행 중인 작업을 중단하고, 대기 중인 작업을 반환하며 즉시 종료한다. 실행 중인 작업을 중단하기 위해 인터럽트를 발생시킨다.
- 논 블로킹 메서드
서비스 상태 확인
- boolean isShutdown()
- 서비스가 종료되었는지 확인한다.
- boolean isTerminated()
- shutdown() , shutdownNow() 호출 후, 모든 작업이 완료되었는지 확인한다.
작업 완료 대기
- boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException
- 서비스 종료 시 모든 작업이 완료될 때까지 대기한다. 이때 지정된 시간까지만 대기한다.
- 블로킹 메서드
- close()
- close()는 자바 19부터 지원하는 서비즈 종료 메서드이다. 이 메서드는 shutdown()과 같다고 생각하면 된다.
- 더 정확히는 shutdown()을 호출하고, 하루를 기다려도 작업이 완료되지 않으면 shutdownNow()를 호출한다.
- 호출한 스레드에 인터럽트가 발생해도 shutdownNow()를 호출한다.
Shutdown() - 처리 중인 작업이 없는 경우

- ExecutorService에 아무런 작업이 없고, 스레드만 2개 대기하고 있다.

- shutdown()을 호출한다.
- ExecutorService는 새로운 요청을 거절한다.
- 거절 시 기본적으로 java.util.concurrent.RejectedExecutionException 예외가 발생한다.
- 스레드 풀의 자원을 정리한다.
Shutdown() - 처리 중인 작업이 있는 경우

- shutdown()을 호출한다.
- ExecutorService는 새로운 요청을 거절한다.
- 스레드 풀의 스레드는 처리 중인 작업을 완료한다.
- 스레드 풀의 스레드는 큐에 남아있는 작업도 모두 꺼내서 완료한다.

- 모든 작업을 완료하면 자원을 정리한다.
- 결과적으로 처리 중이던 taskA , taskB는 물론이고, 큐에 대기 중이던 taskC , taskD 도 완료된다.
ShutdownNow() - 처리 중인 작업이 있는 경우

- shutdownNow()를 호출한다.
- ExecutorService는 새로운 요청을 거절한다.
- 큐를 비우면서, 큐에 있는 작업을 모두 꺼내서 컬렉션으로 반환한다.
- List<Runnable> runnables = es.shutdownNow()
- 작업 중인 스레드에 인터럽트가 발생한다.
- 작업 중인 taskA , taskB는 인터럽트가 걸린다.
- 큐에 대기중인 taskC , taskD는 수행되지 않는다.
- 작업을 완료하면 자원을 정리한다.
구현
shutdown() 을 호출해서 이미 들어온 모든 작업을 다 처리하고 서비스를 우아하게 종료(graceful shutdown)하는 것이 가장 이상적이지만, 갑자기 요청이 너무 많이 들어와서 큐에 대기 중인 작업이 너무 많아 작업 완료 어렵거나, 작업 이 너무 오래 걸리거나, 또는 버그가 발생해서 특정 작업이 끝나지 않을 수 있다. 이렇게 되면 서비스가 너무 늦게 종료되거나, 종료되지 않는 문제가 발생할 수 있다. 이럴 때는 보통 우아하게 종료하는 시간을 정한다. 예를 들어서 60초까지는 작업을 다 처리할 수 있게 기다리는 것이다. 그리고 60초가 지나면, 무언가 문제가 있다고 가정하고 shutdownNow()를 호출해서 작업들을 강제로 종료한다.
close()
close() 의 경우 이렇게 구현되어 있다. shutdown()을 호출하고, 하루를 기다려도 작업이 완료되지 않으면 shutdownNow()를 호출한다. 그런데 대부분 하루를 기다릴 수는 없을 것이다. 방금 설명한데로 우선은 shutdown()을 우아한 종료를 시도하고, 10초간 종료되지 않으면 shutdownNow() 통해 강제 종료하는 방식을 구현해보자. (학습용 예제에서 60초는 너무 길다.) 참고로 구현할 shutdownAndAwaitTermination() 은 ExecutorService 공식 API 문서에서 제안하는 방식이다.


- 작업 처리 필요한 시간
- taskA , taskB , taskC : 1초
- longTask : 100초
- es.shutdown()
- 새로운 작업을 받지 않는다. 처리 중이거나, 큐에 이미 대기 중인 작업은 처리한다. 이후에 풀의 스레드를 종료한다.
- shutdown() 은 블로킹 메서드가 아니다. 서비스가 종료될 때 까지 main 스레드가 대기하지 않는다. main 스레드는 바로 다음 코드를 호출한다.
- es.awaitTermination(10, TimeUnit.SECONDS)
- 블로킹 메서드이다.
- main 스레드는 대기하며 서비스 종료를 10초간 기다린다.
- 만약 10초 안에 모든 작업이 완료된다면 true를 반환한다.
- 여기서 taskA , taskB , taskC의 수행이 완료된다. 그런데 longTask는 10초가 지나도 완료되지 않았다.
- 따라서false를 반환한다.
- 서비스 정상 종료 실패 강제 종료 시도
- 정상 종료가 10초 이상 너무 오래 걸렸다.
- shutdownNow()를 통해 강제 종료에 들어간다. shutdown() 과 마찬가지로 블로킹 메서드가 아니다.
- 강제 종료를 하면 작업 중인 스레드에 인터럽트가 발생한다. 다음 로그를 통해 인터럽트를 확인할 수 있다.
- 인터럽트가 발생하면서 스레드도 작업을 종료하고, shutdownNow() 를 통한 강제 shutdown도 완료된다.
서비스 종료 실패 코드 존재하는 이유
그런데 마지막에 강제 종료인 es.shutdownNow()를 호출한 다음에 왜 10초간 또 기다릴까? shutdownNow()가 작업 중인 스레드에 인터럽트를 호출하는 것은 맞다. 인터럽트를 호출하더라도 여러 가지 이유로 작업에 시간이 걸릴 수 있다. 인터럽트 이후에 자원을 정리하는 어떤 간단한 작업을 수행할 수 도 있다. 이런 시간을 기다려주는 것이다. 극단적이지만 최악의 경우 스레드가 다음과 같이 인터럽트를 받을 수 없는 코드를 수행중일 수 있다. 이 경우 인터럽트 예외가 발생하지 않고, 스레드도 계속 수행된다.
이런 경우를 대비해서 강제 종료 후 10초간 대기해도 작업이 완료되지 않으면 "서비스가 종료되지 않았습니다"라고 개발자가 인지할 수 있는 로그를 남겨두어야 한다. 그래야 개발자가 나중에 문제를 찾아서 코드를 수정할 수 있다.
'멀티스레드와 동시성' 카테고리의 다른 글
| Ch12. 스레드 풀과 Executor 프레임워크 - Excutor 전략 (0) | 2024.10.29 |
|---|---|
| Ch12. 스레드 풀과 Executor 프레임워크 - Executor 스레드 풀 관리 (0) | 2024.10.16 |
| Ch12. 스레드 풀과 Executor 프레임워크 - ExecutorService(작업 컬렉션 처리) (0) | 2024.10.13 |
| Ch12. 스레드 풀과 Executor 프레임워크 - Future (0) | 2024.10.06 |
| Ch12. 스레드 풀과 Executor 프레임워크 - Executor 프레임워크와 ExecutorService (0) | 2024.09.25 |