1. 개요
하나의 자바 프로세스 안에서 여러 스레드이 동작한다. 이 스레드들은 스레드 스케줄러에 실행된다. 그렇다면 프로그램의 동작을 스레드 스케줄러에 기대지 말라는 뜻이 뭘까?
스레드 스케줄러
운영체제가 프로세스 내 스레드들 사이에서 CPU 시간을 어떻게 분배할지를 결정하는 주체이다. 자바 코드 레벨에서 스레드를 생성해도 마찬가지이다. 스레드 스케줄러의 정책에 따라 실행된다. 정책으로는 라운드 로빈 방식, 우선순위 기반 방식, 최소 남은 시간 우선 방식 등이 있다.
2. 운영체제마다 다른 스케줄링 정책
스레드 스케줄러의 정책은 운영체제마다 다를 수 있다. 프로그램의 동작이 스레드 스케줄러에 기댄다면 운영체제의 스케줄링 정책에 영향을 받게 되는것이다. 즉, '스레드 스케줄러에 기대는 프로그램은 정확성이나 성능이 운영 체제에 따라 달라지게 된다' 는 점에서 프로그램을 다른 플랫폼에 이식하기 어렵게 된다.
3. 스레드 스케줄러에 영향을 받지 않으려면 어떻게하나요?
1) 실행 가능한 스레드의 수를 지나치게 만들지 않기
실행 가능한 스레드
실행 가능한 스레드는 스레드 큐에 위치하여 언제든 CPU의 선택을 받을 수 있는 스레드를 말한다.
실행 가능한 스레드의 평균적인 수를 프로세서 수보다 지나치게 많아지지 않도록 하는 것이다. 프로세서는 CPU인데, 멀티쓰레드를 지원하는 JVM 에서 스레드 수를 프로세서 수보다 지나치게 많아지지 않도록 하라는 게 무슨 의미인지 이해되지 않았다. 예를들어 필자의 컴퓨터는 멀티코어로 두개의 CPU가 있다. 그럼 스레드 수를 2개보다 지나치게 많아지지 않도록해라? 아무리 생각해도 100개의 스레드는 지나치게 많은 개수같은데, 일반적으로 JVM 스레드가 100개를 넘는 일은 허다하지 않나라는 의문이 들었다.
위 내용에 대해 고민을 많이 했는데, 책을 다시 보니 필자가 핀트를 잘못 짚은것이었다. 실행 가능한 스레드의 수가 많으면 좋지 않다는게 아니라 단순히 스레드 스케줄러의 영향을 받지 않는 방법을 명시한 것이었다. 스레드 개수가 적어 스케줄러가 고민할 거리가 줄어든다면 정책의 영향을 덜받지 않겠는가. 당연한 얘기였다.
다시 돌아가 필자의 컴퓨터는 한 코어에 6개의 스레드를 동작시킬 수 있다. 멀티 코어이니 최대 12개의 스레드를 동시처리 할 수 있다. 이보다 지나치게 많아졌을 때 스레드 스케줄러에 의해 스레드들이 제어될것이고, 이 프로그램이 '스레드 스케줄러에 기대도록' 구현이 되어있다면 문제가 생길 수 있다는 것이 포인트였다.
2) 스레드는 맡은 작업을 완료할 때까지 계속 실행되도록 하자
스레드가 작업을 완료하기 전에 대기 상태로 가는 코드가 없도록 코드를 작성해야한다. 대기한다는 뜻은 곧 스레드 스케줄러의 정책에 의해 제어된다는 의미이기 때문이다. 또한 스레드는 당장 처리해야할 작업이 없다면 실행되게 해서도 안된다.
4. 당장 처리해야할 작업이 없다면 실행되게 해선 안된다?
당장 처리할 작업이 없다면 대기 상태에 들어갈것이다. 그리고 다시 실행되기 위해 지속적인 상태체크를 한다. 즉, 스레드가 바쁜 대기(busy waiting) 상태가 되는것이다.
당장 처리해야할 작업이 없지만 상태를 검사하기 위해 스레드가 실행된다. 의미없이 CPU를 선점하고 있는 격이기에 의미있는 작업을 맡은 스레드들이 당장 실행될 기회를 박탈당하게 된다.
5. Busy Waiting 예시
CountDownLatch 클래스의 기능을 가진 커스텀 클래스를 만들었다. Busy Waiting 상태가 나타나는 코드로CountDownLatch 클래스를 삐딱하게 구현한 코드이다.
await() 메서드 내 while(true)에 의해 해당 스레드는 계속해서 CPU를 점유할것으로 보이지만, 실제로는 스케줄링 정책에 대기/실행 상태를 왔다갔다하며 다른 스레드들도 중간 중간 실행될것이다. 그런데 이 메서드가 하는 일이 뭔가? 그냥 대기하는것이다. while 문에 의해 count 값만 계속 체크하는 로직인데, countDown()을 수행하는 스레드와 바쁘게 경쟁하게 된다. 경쟁자가 많다보니 컨텍스트 스위칭이 빈번하게 일어날테고, countDown() 호출이 늦어지는 결과를 초래하게 된다.
이런 프로그램이 프로그램의 동작을 스레드 스케줄러에게 기댄 코드라고 할 수 있다.
1) SlowCountDownLatch.java
public class SlowCountDownLatch {
private int count;
public SlowCountDownLatch(int count){
if(count < 0){
throw new IllegalArgumentException(count + " < 0");
}
this.count = count;
}
public void await(){
while(true){
synchronized (this){
if (count == 0)
return;
}
}
}
public synchronized void countDown(){
if (count != 0){
count --;
}
}
}
1) Main.java
public static void main(String[] args) {
SlowCountDownLatch ready = new SlowCountDownLatch(1000);
SlowCountDownLatch start = new SlowCountDownLatch(1);
SlowCountDownLatch end = new SlowCountDownLatch(1000);
ExecutorService executorService = Executors.newFixedThreadPool(1000);
for(int i =0; i< 1000; i++){
executorService.submit(() -> {
ready.countDown();
start.await();
System.out.println("끼요옥");
end.countDown();
});
}
ready.await();
System.out.println("준비완료. 시작!");
start.countDown();
long StartNanos = System.nanoTime();
end.await();
System.out.println("걸린시간 : " + ( System.nanoTime() - StartNanos));
}
스레드 1000개를 생성하여 SlowCountDownLatch를 테스트한 결과, Java에서 제공하는 CountDownLatch보다 2, 3배 느린 것을 확인할 수 있었다. 책에서는 약 10배가 느렸다고 한다.
6. 실행을 양보한다면?
그럼 await() 내 상태 체크가 실패했을 경우 현재 스레드를 해제하고 다른 스레드가 작업할 수 있도록 '양보'하는 건 어떨까? 현재는 운영체제의 스레드 스케줄링 정책에 의해 대기상태로 가고 있는데, 당장 스레드 큐로 보내버리는 것이다.
Thread.yield() 메서드를 이용하면 현재 스레드를 실행 가능한 상태로 보낼 수 있다.
public void await(){
while(true){
synchronized (this){
if (count == 0)
return;
else{
Thread.yield();
}
}
}
}
여기서 중요한 점은 Thread.yield()는 무작정 양보 되는 것이 아니라, 운영체제에게 '요청'하는 메서드이다. 운영체제의 스레드 스케줄러가 이를 무시할 수 있다. 거절당할 수 있는 것이다. 또한, 운영체제마다 yield()의 동작이 달라질 수도 있다는 점에서 OS에 대한 이식성이 좋지 않으며. 호출이 너무 많아지면 컨텍스트 스위칭이 많아져 성능 저하를 초래할 수 있다. 좋지 않은 방법이다.
스레드의 우선순위를 조절하는 것도 방법인데, 이것도 OS에 대한 이식성이 좋지 않다. 특히 심각한 문제를 스레드 우선순위로 해결하려는 시도는 합리적이지 않다.
7. 정리
프로그램의 동작을 스레드 스케줄러에 기대지 말자. 성능과 속도, 이식성을 모두 해치는 행위이다. 같은 이유로 Thread.yield() 와 스레드 우선순위에 의존해서도 안된다.
스레드 우선순위는 이미 잘 동작하는 프로그램의 서비스 품질을 높이기 위해 드물게 쓰일 수는 있지만, 간신히 동작하는 프로그램을 '고치는 용도'로 사용해서는 안된다.
'공부 > Effective Java' 카테고리의 다른 글
[Effective Java] readObject 메서드는 방어적으로 작성하라 / 역직렬화 / readObject (0) | 2024.11.07 |
---|---|
[Effective Java] Item 82. 스레드 안전성 수준을 문서화하라 (0) | 2024.10.03 |
[Effective Java] Item 73. 추상화 수준에 맞는 예외를 던져라 (0) | 2024.08.22 |
[Effective Java] Item 72. 표준 예외를 사용하라 (0) | 2024.08.15 |
[Effective Java] Item 70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라 (0) | 2024.07.04 |