두 개의 작업을 하나의 쓰레드로 처리하는 경우 vs 두 개의 쓰레드로 처리하는 경우
싱글 쓰레드를 사용하면 하나의 작업이 완료된 후에 다음 작업을 시작할 수 있지만, 2개의 쓰레드로 하면 쓰레드가 번갈아가며 2개의 작업을 모두 진행시켜 마치 동시에 작업이 처리되는 것처럼 보임.
그러나, 쓰레드가 2개면 오히려 쓰레드 간의 작업 전환(Context Switching) 때문에 싱글 쓰레드일 때보다 시간이 더 걸리게 됨.
Context Switching
진행하고 있던 작업(프로세스 or 쓰레드)의 상태를 저장하고, 다음 작업의 상태를 읽어오는 과정
그래서 항상 멀티 쓰레드가 좋은 것은 아니고, 상황에 따라 싱글 쓰레드로 프로그래밍 하는 것이 나을 수 있음.
싱글 쓰레드로 2개의 작업 진행
class SingleThreadExample {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < 2000; i++) {
System.out.printf("%s", new String("-"));
// printf()와 new String()을 사용해서 고의로 속도를 늦췄음.
}
System.out.println("소요 시간1: " + (System.currentTimeMillis() - startTime));
for (int i = 0; i < 2000; i++) {
System.out.printf("%s", new String("|"));
}
System.out.println("소요 시간2: " + (System.currentTimeMillis() - startTime));
}
}
실행 결과)
(너무 길어서 앞부분 생략)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------소요 시간1: 21
(여기도 앞부분 생략)
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||소요 시간2: 32
Process finished with exit code 0
2개의 작업을 전부 마치는데 32ms 소요
멀티 쓰레드(2개)로 2개의 작업 진행
class MultiThreadExample {
static long startTime = 0;
public static void main(String[] args) {
Thread1 t1 = new Thread1();
t1.start();
startTime = System.currentTimeMillis();
for (int i = 0; i < 2000; i++) {
System.out.printf("%s", new String("-"));
}
System.out.println("소요 시간1: " + (System.currentTimeMillis() - startTime));
}
}
class Thread1 extends Thread {
public void run() {
for (int i = 0; i < 2000; i++) {
System.out.printf("%s", new String("|"));
}
System.out.println("소요 시간2: " + (System.currentTimeMillis() - MultiThreadExample.startTime));
}
}
실행 결과)
(너무 길어서 앞부분 생략)----||||-----|||-------|||||----------------------||---------------|||--|||||--|||---------------------||||---------||---||----|||||---||||||||--||||------||--|||||||||||||-----||||||||||---------------------------||||||||||||||||---------|||||||||||||||||--------------||---|||-----||---||--|||||||||||||||||||||||||||----|||||||||||||||||||||------||||||||||---------------------------------------------------------------------------------------------------------------------------------------------------------------------------소요 시간2: 40
소요 시간1: 41
2개의 작업을 전부 마치는데 41ms 소요
8코어 CPU 환경에서 테스트 했기 때문에, 개별적인 코어에서 각각 쓰레드가 실행되어 동시에 2개의 작업이 이루어짐
출력 화면(console)이라는 자원을 공유하고 있으므로 각 쓰레드가 자원을 두고 경쟁하며
-
와|
출력이 난잡하게 번갈아 나오는 것.
결론: 같은 자원을 사용하는 2개의 작업을 처리할 때는 싱글 쓰레드의 작업 속도가 더 빠름.
📌 [참고]
병행(concurrent): 단일 코어 안에서 작업을 여러 개의 쓰레드로 쪼개서 번갈아가며 수행하는 것. (물리적으로 동시에 작업이 이루어지는 것은 아니지만, 번갈아가는 속도가 빠르기 때문에 동시에 처리되는 것처럼 보임)
병렬(parallel): 멀티 코어 환경에서 작업을 여러 개의 코어로 분산시켜서 각 코어의 쓰레드로 동시에 작업하는 것 (실제 물리적으로 여러 작업이 동시에 실행되는 것)
📌 [참고]
예제의 실행 결과에서 측정되는 소요 시간은 매번 다르게 나오는데, 실행 중인 예제 프로세스가 OS의 프로세스 스케줄러의 영향을 받기 때문.프로세스 스케줄러는 프로세스의 실행 순서와 실행 시간을 결정하는데, 상황에 따라 매번 다르게 결정하고 이에 따라 쓰레드에게 할당되는 시간도 일정하지 않게 됨.
반면, 서로 다른 자원을 사용하는 2개의 작업을 처리할 때는 멀티 쓰레드의 작업 속도가 더 빠름.
ex) 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터 등의 외부기기와 입출력을 하는 작업...
사용자로부터 입력값을 받아와 출력하는 작업과, 숫자를 10부터 1까지 1초마다 세는 작업
class ThreadExample {
public static void main(String[] args) throws Exception{
String input = JOptionPane.showInputDialog("Enter any value");
System.out.println("Your input is: " + input);
for (int i = 10; i > 0; i--) {
System.out.println(i);
try {
Thread.sleep(1000); // 1초 동안 시간 지연
} catch (Exception e) {}
}
}
}
실행 결과)
Your input is: hello
10
9
8
7
6
5
4
3
2
1
입력값 받아와 출력하는 것과 숫자를 세는 것은 아무런 공유 자원이 없는 완전히 별개의 작업임.
그러나 싱글 쓰레드라서 입력값을 받아오기 전까지는 다른 작업을 수행할 수 없어 매우 비효율적.
class ThreadExample {
public static void main(String[] args) throws Exception{
Thread1 t1 = new Thread1();
t1.start();
String input = JOptionPane.showInputDialog("Enter any value");
System.out.println("Your input is: " + input);
}
}
class Thread1 extends Thread {
public void run() {
for (int i = 10; i > 0; i--) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (Exception e) {}
}
}
}
실행 결과)
10
9
8
7
6
Your input is: hello
5
4
3
2
1
이번에는 두 작업을 각각 다른 쓰레드로 나누어 실행했기 때문에 입력값이 들어와 출력이 되었는지와 관계 없이 숫자 세기 작업이 진행됨.
쓰레드가 노는 시간을 없앴기 때문에 훨씬 효율적인 CPU 활용 방식.