ScheduledThreadPoolExecutor가 작동하는 원리

2024. 4. 25. 19:35Backend/Spring

ScheduledThreadPoolExecutor가 작동하는 원리를 이해하기 위해 먼저 ThreadPoolExecutor의 동작을 이해해보겠습니다.

 

ThreadPoolExecutor가 작동되는 원리

Worker: ThreadPoolExecutor에 의해 관리되는 각 스레드 인스턴스.

Worker run메소드가 실행되면 runWorker가 작동합니다.

ThreadPoolExecutor의 runWorker()

 

runWoker()가 동작되면

현재 스레드를 가져오고, while 루프 내에서 Worker.getTask() 메서드를 통해 workQueue에 있는 task를 계속 가져와서 task.run()을 진행합니다.

Worker의 getTask()

getTask()에서 일시적으로 workQueue에 작업이 없으면 끝나는 거 아니야? 라고 생각할 수도 있지만, 

 

keepAliveTime 설정이 충분히 길게 되어 있다면, 일시적으로 작업이 없더라도 스레드는 대기 상태를 유지할 수 있습니다.

 

keepAliveTime은 corePoolSize를 초과하는 스레드가 얼마나 오랫동안 작업을 기다릴 수 있는지를 결정하는 시간입니다. 이 시간이 지나면 활동하지 않는 스레드는 종료되지만, ThreadPool 자체는 계속 살아 있어서 새로운 작업을 받을 준비가 되어 있습니다.

 

또한, allowCoreThreadTimeOut이 false로 설정되어 있거나, 스레드 수가 corePoolSize 이하인 경우, 스레드는 타임아웃 없이 계속 대기합니다. 즉, 이러한 설정에서는 스레드가 작업을 기다리며 계속해서 대기할 수 있게 됩니다.

 

만약 allowCoreThreadTimeOut이 true로 설정된 경우, corePoolSize 내의 스레드도 keepAliveTime이 지나면 제거될 수 있습니다. 그러나 이 경우에도 새로운 작업이 도착하면 addWorker() 메소드가 호출되어 새로운 worker가 생성됩니다. 따라서 일시적으로 작업이 없더라도 ThreadPool은 새로운 작업을 처리하기 위해 계속 작동할 수 있습니다.

 

ThredPoolExecutor.execute(Runnable command) 메서드 내부

또한

  • timed == true 일 경우, poll() 메소드를 사용해 지정된 keepAliveTime 동안 대기하고, 그 시간이 지나면 null을 반환하여 스레드가 종료될 수 있게 합니다.
  • timed == false 일 경우, take() 메소드를 사용해 작업이 사용가능할 때까지 무한히 대기합니다. 이는 핵심 스레드에 대한 처리 방식이며, 이 스레드들은 일반적으로 스레드 풀이 활성 상태인 동안 계속 존재합니다.

 

결국 getTask()를 통해 ThreadPoolExecutor는 작업을 수행하게 되는데, 다음에 설명드릴 ScheduledExecutorService는 다른 workQueue 구현체의 poll()부분의 특성을 가집니다.


ScheduledThreadPoolExecutor와 DelayedWorkQueue

이러한 ExecutorService를 보다 더 특정 시점에 작업을 실행할 수 있도록 하거나, 정기적으로 반복되는 작업을 관리할 수 있도록 설계된 ScheduledThreadPoolExecutor가 존재합니다.

 

ScheduledThreadPoolExecutor는 기존 ThreadPoolExecutor에서 사용하던 BlockingQueue를 구현하여 내부적으로 MinHeap의 형태를 가지는 DelayedWorkQueue를 사용합니다.

 

DelayedWorkQueue 

DelayedWorkQueue는 delayedTime을 기준으로 정렬된 상태를 유지합니다. (MinHeap)

이러한 DelayedWorkQueue를 ThreadPoolExecutor의 생성자에서 Override하여 사용합니다.

DelayedWorkQueue.poll()

DelayedWorkQueue는 poll 메소드에서, 

큐의 상태를 반복적으로 확인하면서 적절한 조건에서 루프를 탈출하거나 계속 실행합니다.

 

delay는 대기열의 맨 앞에 있는 작업이 실행될 때까지 남은 시간을 뜻합니다.

nanos는 메소드에 전달된 전체 대기 시간이고, 최대로 대기할 수 있는 시간을 뜻합니다.

 

delay가 남아 있고 nanos도 0 이상이면, 스레드는 delay 또는 남은 nanos 중 더 작은 시간만큼 대기합니다.

 

timeleft = available.awaitNanos(delay)부분의 코드로 특정 시간까지 대기하게 됩니다.

Condition 인터페이스의 awaitNanos

현재 Thread를 signal이나 interrupted까지 wait하게 만듭니다.

 

지정된 시간이 종료되면 스레드가 깨어나게 되고, 반복문이 다시 동작하여 delay가 0이하가 될 것이고(작업 지연시간이 만료되면) finishPoll()을 진행합니다. 

 

더 짧은 우선순위의 task가 들어오는 경우

만약 기존 스레드가 wait상태에서 더 짧은 우선순위의 task가 들어오게 되면

ScheduledThreadPoolExecutor 의 DelayedWorkQueue.offer()

 

offer() 메소드 내에서 available.signal()을 통해 스레드를 wait상태에서 깨우고, 다음 반복문으로 가게 되면

first가 새롭게 queue[0]를 가리키게 될 것이므로 더 짧은 우선순위의 작업을 수행하게 됩니다.

 

결국 poll() 메소드의 대기하는 특성으로 ScheduledThreadPoolExecutor는 효과적으로 정해진 시간에 동작하는 Task를 효과적으로 수행하게 됩니다. 

 

하나의 리더 스레드만이 정렬된 queue를 바탕으로 효과적으로 작업 수행을 관리하는 ScheduledThreadPoolExecutor의 특성을 알아보았습니다.