84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라
스레드 스케줄러는 여러 스레드가 실행 중일 때 어떤 스레드를 얼마나 오래 실행할지 결정하는 운영체제의 한 부분이다.
마치 놀이터에서 여러 친구들이 미끄럼틀을 타려고 할 때, 선생님이 누구를 먼저 태우고 얼마나 오래 타게 할지 결정하는 것과 비슷하다.
// 스레드를 사용하는 간단한 예시
public class ThreadExample {
public static void main(String[] args) {
// 두 개의 스레드 생성
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("스레드 1 실행 중...");
try {
Thread.sleep(1000); // 1초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("스레드 2 실행 중...");
try {
Thread.sleep(1000); // 1초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 스레드 시작
thread1.start();
thread2.start();
}
}
스레드 스케줄러에 의존하면 안 되는 이유
다른 운영체제, 다른 동작: 윈도우, 맥, 리눅스 등 각 운영체제는 스레드를 관리하는 방식이 다르다.
예측 불가능성: 같은 코드라도 다른 환경에서 실행하면 스레드 실행 순서나 시간이 달라질 수 있다.
이식성 문제: 스레드 스케줄러에 의존하는 프로그램은 한 컴퓨터에서는 잘 동작하더라도 다른 컴퓨터에서는 문제가 생길 수 있다.
이런 이유로 프로그램의 정확성이나 성능이 스레드 스케줄러에 의존하지 않도록 설계하는 것이 중요하다.
스레드 스케줄러를 직접 제어하려 하기보다는, 스레드 수를 적절히 유지하고 표준 동시성 유틸리티를 사용하는 것이 좋은 방법이다.
좋은 멀티스레드 프로그램 작성법
좋은 멀티스레드 프로그램을 만들기 위한 두 가지 핵심 원칙
실행 가능한 스레드 수를 적게 유지하기
컴퓨터의 CPU 코어 수보다 너무 많은 스레드를 만들지 않기
스레드가 할 일이 없을 때는 대기 상태에 두기
바쁜 대기(busy waiting) 피하기
바쁜 대기란 스레드가 계속해서 무언가 확인하며 CPU를 쓰는 상태
나쁜 예시
// 나쁜 예: 바쁜 대기를 사용하는 코드
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--;
}
}
좋은 예시
// 좋은 예: 자바의 CountDownLatch 사용
import java.util.concurrent.CountDownLatch;
public class GoodExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3); // 3개의 작업이 끝나기를 기다림
// 3개의 작업 스레드 생성
for (int i = 0; i < 3; i++) {
final int taskNum = i;
new Thread(() -> {
try {
// 작업 수행
System.out.println("작업 " + taskNum + " 수행 중...");
Thread.sleep(1000); // 작업 시간
System.out.println("작업 " + taskNum + " 완료!");
// 작업 완료 신호
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
// 모든 작업이 완료될 때까지 대기
System.out.println("모든 작업이 완료되기를 기다립니다...");
latch.await();
System.out.println("모든 작업이 완료되었습니다!");
}
}
피해야 할 것들
1. Thread.yield() 사용하지 않기
Thread.yield()
는 현재 실행 중인 스레드가 다른 스레드에게 CPU 사용을 양보하는 메서드다.
하지만 이 메서드는 운영체제마다 다르게 동작할 수 있어 이식성이 떨어진다.
// Thread.yield()를 사용한 나쁜 예
public class YieldExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("작업 수행 중...");
// CPU를 다른 스레드에 양보하려는 시도 (하지만 효과가 보장되지 않음)
Thread.yield();
}
});
thread.start();
}
}
2. 스레드 우선순위에 의존하지 않기
스레드 우선순위는 스레드 스케줄러에게 어떤 스레드를 더 자주 실행해야 하는지 알려주는 힌트다.
하지만 이 또한 운영체제마다 다르게 동작한다.
// 스레드 우선순위를 사용한 나쁜 예
public class PriorityExample {
public static void main(String[] args) {
Thread highPriorityThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("높은 우선순위 스레드 실행 중...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread lowPriorityThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("낮은 우선순위 스레드 실행 중...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 우선순위 설정 (효과가 보장되지 않음)
highPriorityThread.setPriority(Thread.MAX_PRIORITY); // 우선순위 10
lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // 우선순위 1
highPriorityThread.start();
lowPriorityThread.start();
}
}
좋은 방법과 나쁜 방법 비교
실행 가능한 스레드 수를 적게 유지
너무 많은 스레드 생성
효율적인 대기 (wait/notify, CountDownLatch 등)
바쁜 대기 (busy waiting)
표준 동시성 유틸리티 사용 (java.util.concurrent)
Thread.yield() 사용
스레드 풀 사용
스레드 우선순위에 의존
정리
프로그램의 동작을 스레드 스케줄러에 기대지 말자.
견고하고 이식성 좋은 프로그램을 만들려면
실행 가능한 스레드 수를 적게 유지하자.
각 스레드가 작업을 완료한 후에는 다음 작업이 생길 때까지 대기하게 하자.
바쁜 대기 상태를 피하자.
Thread.yield()와 스레드 우선순위에 의존하지 말자.
🧩 어려웠던 점
스레드 스케줄러의 동작 방식을 이해하는 것이 가장 어려웠다.
특히 바쁜 대기(busy waiting)가 왜 문제인지 이해하는 데 시간이 걸렸다.
💡 느낀 점
멀티스레드 프로그래밍은 생각보다 복잡하다는 것을 느꼈다.
단순히 코드가 돌아가게 만드는 것보다 견고하고 이식성 있는 코드를 만드는 것이 중요하다는 점을 깨달았다.
"빠르게 가려면 혼자 가고, 멀리 가려면 함께 가라"는 말처럼, 단기적인 성능 향상을 위해 스레드 스케줄러에 의존하기보다는 표준적인 방법을 사용하는 것이 장기적으로 더 좋은 선택이라는 것을 배웠다.
Last updated