본문 바로가기

Java/JAVA

자바 쓰레드를 거의 정확한 주기로 실행하는 방법!(로직 수행 시간에 관계없이 정확한 주기 만들기,Timer, ScheduledExecutorService)

반응형

자바 쓰레드를 거의 정확한 주기로 실행하는 방법 (사족)

"정확한 주기"로 실행하는 방법을 쓰게된 이유는 거의 비슷할 것이다.

어떤 애플리케이션을 개발할 때 백그라운드에서 일정 주기마다 어떤 값을 검사하거나 보내는 등의 특정 로직을 수행하는 기능(쓰레드)이 필요하기 때문이다.

나 역시도 데이터를 짧은 주기마다 보내야하는 로직이 필요했다.

그래서 쓰레드의 주기하면 단순하게 떠오르는 것이 Thread.sleep() 메소드였다.


- sleep()으로 될까?

단순하게 쓰레드의 run() 메소드에 sleep(1000);을 적어서 1초쉬고 해당로직을 수행하게 했다.

시간(주기)이 애플리케이션에서 치명적이지 않다면 간단하면서 쉬운 방법이다.

하지만 시간이 정확해야하는 애플리케이션에서는 로직 수행시간이 추가되면서 지속적인 오차 또는 시간 밀림 현상이 나타난다.

  1초 != sleep(1000) + 로직 수행 시간


- 동적 sleep()을 줘볼까?

그래서 생각해낸게 로직 수행 시간을 계산해서 주기(1초)에서 뺀 만큼만 sleep()을 걸면 어떨까? 라는 생각이었다.

예를들어 로직수행시간이 0.1초가 걸리면 0.9초(sleep(900))만큼 쓰레드를 중지시키는 것이다.

이제 걸리는 시간을 System.nanoTime() 를 사용해서 계산했다. (System.currentTimeMillis()로는 계산이 불가할 정도로 짧은 수행시간..)

계산 결과를 적용할 수 있는 sleep이 있나 찾아보니 실제로 Thread.sleep(long , long) 메소드가 있었다.

첫 번째 파라미터는 밀리초 단위의 long값이고 두 번째 파라미터는 나노초 단위의 long값이다.

이 메소드는 밀리초 + 나노초 시간 만큼 sleep한다. (단, 뒤에 나노초는 ns<1,000,000)

결론부터 말하면 1밀리초정도 밀린다.

* 계산하는 로직(실험에서는 for문 100만번 반복하며 + 연산)이 실행되는 환경마다도 다르고 수행시간을 계산하는 시간은 또 포함이 안되었기 때문에 보정을 해줘서 조금 덜 밀릴수는 있지만 어쨋든 밀린다.


- setInterval(), 콜백 메소드 같이 쉬운거 없나? 

자바스크립트가 그리웠다... 콜백메서드로 해당 주기로 실행만 위임시키고 싶었다..


자바 쓰레드를 거의 정확한 주기로 실행하는 방법

sleep()을 버리고 적당한 것을 찾았다.

바로 Timer 클래스를 이용하는 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.Timer;
import java.util.TimerTask;
public class JavaTimerTest {
    public static void main(String[] args) {
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                int result = 0;
                // 특정 로직
                for(int i=0;i<10000;i++) {
                    for(int j=0;j<10000;j++) {
                        result = i+j;
                    }
                }
                //이부분에 send()같은 것이 들어갈 것
                System.out.println(System.currentTimeMillis());
            }
        };
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(task, 01000);
    }
}


TimerTask를 만들고 특정로직은 for로 100,000,000번 반복하는 코드로 대체했다.

그리고 timer객체의 .scheduleAtFixedRate(task, delay, period) 메소드를 호출했다.

해당 메소드는 task를 delay만큼 지연시킨 후에 period마다 실행하는 메소드다.

currentTimeMillis()로 찍은 내용으로 끝에서 4번째 자리를 통해 1초 주기마다 정확히 찍고 있는 것을 알 수 있고 이하 자리를 보면, 특히 가장 마지막자리인 1밀리초단위를 보면 9 또는 0으로 미묘한 차이만 있을 뿐 거의 정확한 주기로 실행되고 있음을 알 수 있다.


하지만 좀 더 알아보니 Timer클래스는 특유의 문제가 있다고 한다.

명확히 그 문제가 무엇인지 찾아보지 않았지만 어쨋든 나는 주기적으로 쓰레드를 여러 개를 돌릴 것이기 때문에 더 정확하고 효율적인 것을 찾아보았다.

바로 ScheduledExecutorService 라는 클래스다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
 
public class JavaTimerTest {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            int result=0;
            @Override
            public void run() {
                for(int i=0;i<10000;i++) {
                    for(int j=0;j<10000;j++) {
                        result = i+j;
                    }
                }
                //이부분에 send()같은 것이 들어갈 것
                System.out.println(System.currentTimeMillis());
            }
        };
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        service.scheduleAtFixedRate(runnable, 0100, TimeUnit.MILLISECONDS);
    }
}
 
cs

해당 서비스를 만들어서 Timer와 유사하게 scheduleAtFixedRate(실행가능객체, 지연시간, 주기, 단위)를 사용한다.

특징은 TimeUnit이라는 단위가 여러가지가 있다. (초, 밀리초, 마이크로초, 나노초 등...)

그래서 해당 주기가 100이지만 밀리초를 썼기때문에 결국 0.1초마다 runnable 객체를 실행하는 것이다.

0.1초 단위이기 때문에 끝에서 세 번째자리를 보면 1씩 올라가는 것을 볼 수 있고, 마지막 자리인 1밀리초 자리도 9~2 사이로 왔다갔다하는 약간의 오차가 있지만 거의 정확하게 0.1초마다 수행하는 것을 알 수 있다.


결과적으로는 자바 쓰레드를 특정 주기마다 수행하고 싶은 경우에 ScheduledExecutorService를 만들어서 사용하면 된다.

위에 처럼 꼭 newSingleThreadScheduledExecutor()로 만들 필요는 없고 쓰레드풀로 만들어 써도 된다.

반응형
  • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2018.04.25 10:46 신고

    Executors.newFixedThreadPool(1); 로 생성하면 쓰레드 시작 개수가 1이고 최대 개수가 1이니까 newSingleThreadExecutor() 와 같고,
    Executors.newFixedThreadPool(10);으로 하면 쓰레드 풀에 쓰레드가 10개 즉, 시작 개수 10개에 최대 개수 10개로 생성된다.
    또한 시작개수가 0개고 최대 개수가 10개로 고정된 쓰레드풀을 만드는 방법은 newCachedThreadPool(10);로 만들면 된다.

  • Favicon of https://dudwns3625.tistory.com BlogIcon dudwns3625 2018.04.30 16:59 신고

    삭제하지 마세요 ㅠㅠ

  • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2018.06.05 23:06 신고

    Timer클래스는 등록된 작업을 실행시키는 쓰레드가 1개 뿐이라 정확한 주기로 실행이 불가한 경우도 많다고 합니다.
    또한 예외처리가 전혀 없기 때문에 쓰레드가 죽을 수도 있다고 합니다.

  • 음... 2019.02.16 22:57

    원래 이렇게 어렵게 사용하는 것인가요?

    Runnbale은 왜 객체로 사용하셨는지 이해가 잘 안되네요.

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2019.02.17 10:50 신고

      쓰레드가 실행할 수 있는 runnable 인터페이스를 확장한 객체를 만들어서 등록시켜주는 것이 일반적인 방법이라고 생각하는데요

      더 쉬운 방법이 있으면 키워드나 힌트주시면 공부하고 공유하도록 하겠습니다.

      왜 객체로 사용했냐는지가 람다나 익명함수로 직접 넣는 것을 원하시는건 아니겠죵... 잘 몰라서요
      일단 예제를 위해서 만든 것이니 양해바랍니다.

  • Favicon of https://code-life.tistory.com BlogIcon Woulk 2019.12.18 12:21 신고

    Timer 여러개를 다른 용도로 사용하고있는데 ScheduledExecutorService로 사용하려면 서비스 객체를 하나로 사용하고 나머지 기존 Timer에 있는 runnable을 하나의 서비스 객체에 scheduleAtFixedRate를 이용하여 각각 호출하면 되나요?

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2019.12.18 20:44 신고

      배치 성격의 주기가 꽤 긴 작업(Runnable)이면 말씀하신대로 하나의 ScheduledExecutorService를 생성하고 여러 작업들을 각각의 interval로 실행시키는 것이 구조적으로 깔끔할 것 같습니다.

  • 너무궁금해요 2020.07.31 00:59

    안녕하세요!! 왜 for문을 100,000,000번이나 돌리셨는지 너무 궁금하네요..ㅎㅎ

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.07.31 10:55 신고

      음.. 충분한 시간이 걸리는 예제를 만들어보려고 했던 것 같습니다.
      주석으로 send()같은 게 들어간다고 한 것처럼 다른 서버의 API 호출과 같은 일종의 (시간이 꽤 소요되는)네트워크 작업이 들어간다고 상상해주길 기대해서 그랬습니다...ㅋㅋ
      사실 1억 번 반복해도 일반적인 컴퓨터 성능에서는 금방 처리되겠지만요...