파이문

TimedSemaphore 를 사용하던 멀티 쓰레드가 종료 안되던 문제 본문

TIL

TimedSemaphore 를 사용하던 멀티 쓰레드가 종료 안되던 문제

민Z 2020. 12. 8. 15:59

ExecutorService 를 사용하여 멀티 쓰레드로 컨슈머를 개발하였다.

컨슈머는 RPS 를 지켜야 하는 룰이 있어서, Apache 의 TimedSemaphore 를 사용하였다.

 

멀티 쓰레드로 돌리는 작업이 완료가 되면 프로세스가 종료 되게끔 만들었으나 종료 되지 않았다.

shutdown 을 하였는데 왜 종료가 되지 않는 것인지 엄청 삽질을 했다.

 

사실 ExecutorService 의 shutdown 메서드 자체가 반드시 쓰레드를 종료 시킨다는 보장은 없다.

(ExecutorService shutdown not working 이 구글 서제스트로 있을 정도이다.)

 

그래서 처음엔 개발을 잘못해서, 쓰레드로 만든 코드에 버그가 있어서 shutdown 으로 종료가 보장이 안되어 shutdownNow 를 호출해야 하는 것인 줄로 파악했다.

 

그런데 코드가 특별히 어려울 것도 없고, 큐가 비어있으면 제대로 종료가 되었다. (????)

큐에서 아이템 하나만 꺼내와도 컨슈머가 종료가 안되는 버그가 있었던 것이다.

 

알고보니 TimedSemaphore 가 내부적으로 ScheduleExceutorService를 사용하고 있었다. 그래서 단순히 멀티 쓰레드로 만든 컨슈머만 shutdown 해서는 안되고 컨슈머가 사용하는 Timedsemaphore 도 shutdown 해줬어야 하는 것이다.

 

큐에서 아이템을 꺼내온 이후에만 컨슈머가 종료되지 않았던 이유는, 내부적으로 아이템이 존재 해야만 semahore 의 acquire 를 호출하게끔 되어 있었는데, TimedSemaphore 의 acquire 또는 tryAquire 를 호출 하면 startTimer 라는 함수를 호출하고 있었다.

    /**
     * Starts the timer. This method is called when {@link #acquire()} is called
     * for the first time. It schedules a task to be executed at fixed rate to
     * monitor the time period specified.
     *
     * @return a future object representing the task scheduled
     */
    protected ScheduledFuture<?> startTimer() {
        return getExecutorService().scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                endOfPeriod();
            }
        }, getPeriod(), getPeriod(), getUnit());
    }

따라서 TimedSemphore 의 객체를 만들어줬어도 사용하지 않으면 (acquire, tryAqcuire 를 호출하지 않으면) startTimer 가 동작하지 않아서 프로세스가 종료 될 수 있었던 것이다.

 

참고로 shutdown 을 호출하고 난 이후에는 acquire, tryAcquire 는 호출하지 못한다.

    /**
     * Prepares an acquire operation. Checks for the current state and starts the internal
     * timer if necessary. This method must be called with the lock of this object held.
     */
    private void prepareAcquire() {
        if (isShutdown()) {
            throw new IllegalStateException("TimedSemaphore is shut down!");
        }

        if (task == null) {
            task = startTimer();
        }
    }

막 사용하지 말고 "API 사용법을 똑바로 읽자" 의 교훈을 다시 한번 깨닫게 되었다.

commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/concurrent/TimedSemaphore.html

Comments