본문 바로가기

Java/Java Language

[Java] Single Thread와 Multi Thread

Multi Threading 개념

Multi Threading란

Multi Threading은 하나의 프로세스 내에서 여러 스레드가 동시에 작업을 수행하는 것이다. 그러나 Multi Threading에서 스레드들은 번갈아가면서 실행되는 경우가 더 일반적이다. 보통 스레드의 수는 언제나 코어의 개수보다 훨씬 많은 환경이기 때문이다.

 

그래서 프로세스의 성능이 단순히 스레드의 개수에 비례하는 것이 아니다. 경우에 따라 하나의 스레드를 가진 프로세스 보다 두 개의 스레드를 가진 프로세스가 오히려 더 낮을 성능을 보일수도 있다.

 

물론, CPU 코어의 개수에 따라 Multi Threading은 병렬로 처리될수 있다. CPU 코어 개수가 실행되는 스레드 개수 이하일떄는 말이다. 실제로 CPU의 코어에서는 한번에 하나의 스레드가 실행될수 있기 때문이다.

Multi Threading의 역할

유투브를 볼때 영상을 재생시키면서 댓글 달기가 가능한 것은 Multi Threading 덕분이다. 동일한 프로세스 내에서 여러 작업을 처리하기 때문이다.

 

위와 같이 대부분 여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 multi thread로 프로그램을 작성하는 것은 필수적이다. 하나의 서버 프로세스가 여러개의 스레드를 생성해서 스레드와 사용자의 요청이 1:1로 처리되도록 프로그래밍 해야한다. 그러면 각 사용자마다 할당된 스레드의 Call Stack으로 독립된인 작업을 실행하여 서비스를 제공할수 있다.

 

만일, single thread로 서버 프로그램을 작성한다면 사용자의 요청마다 새로운 프로세스를 생성해야하기 때문이다. 이는 프로세스를 생성하는 것은 스레드를 생성하는 것보다 많은 시간과 메모리 공간이 필요하다. 따라서 많은 수의 사용자 요청을 서비스하기는 어렵다.

 

각 스레드는 함수의 실행 컨텍스트를 위한 고유한 stack을 가지고 있으며, 여러 스레드가 하나의 프로세스 내에서 자원(코드, 열린 파일, static 변수 등)을 공유함으로써 메모리 사용을 최소화한다. 그리고 이전까지 진행했던 스레드의 작업을 그대로 이어서 실행하기 위해서, 스레드를 나타내는 PCB에 CPU 코어의 레지스터값(PC, registers)들을 저장한다. 

 

그러나 Multi Threading에서 스레드간의 공유하는 자원 때문에 synchronization deadlock이 발생할 수 있다. 따라서 Multi Thread를 사용해서 개발할때는 신중하게 프로그래밍 해야한다.

  • deadlock: 두 스레드가 자원을 점유한 상태에서 서로 상대편의 자원을 사용할려고 대기하여 진행이 멈춘것이다.
  • synchronization: 여러 스레드가 동시에 자원을 접근해서 수정되어, 데이터 일관성이 깨지거나 예기치 않은 동작이 발생하는 것이다.

Multi Threading의 장점

  • 작은 단위의 스레드로 코드를 실행시켜 CPU 사용률을 향상시킨다.
  • 여러 스레드간에 자원을 공유하여 자원을 보다 효율적으로 사용
  • 사용자의 요청마다 작은 단위의 스레드를 생성해서 처리하여, 사용자에 대한 응답성 향상
  • 각 스레드간의 작업이 분리되어 코드가 간결해진다.

Single Thread and Multi Thread

Single Core에서 Single Thread와 Multi Thread의 비교

SingleThread와 MultiThread의 차이를 비교해보기 위해서, 싱글 코어에서 2개의 작업(A, B)을 하나의 스레드(th1)로 처리하는 경우와 두 개의 스레드(th1, th2)로 처리하는 경우를 살펴보자. 

 

하나의 스레드(th1)로 2개의 작업을 처리하면, A 작업을 마친 후에 B 작업을 시작한다. 두 개의 스레드(th1, th2)는 2개의 작업을 처리하면, 짧은 시간동안 A와 B 작업을 번갈아가면서 수행한다.

 

결국, 2개의 작업을 하나의 스레드로 처리한것과 두개의 스레드로 처리한 시간은 거의 비슷할 것이다. 그러나 실제로는 경우에 따라, 두 개의 스레드로 작업한 시간이 싱글스레드로 작업한 시간보다 더 걸릴수도 있다. 그 이유는 스레드간의 전환(Context Switching)이 있었기 때문이다. 자세한 내용은 Scheduling and Context Switching에서 Context Switching 부분에 있다.

 

그래서 싱글 코어에서 단순히 CPU을 사용하는 계산작업이라면, 오히려 멀티 스레드보다 싱글스레드로 프로그래밍하는 것이 더 효율적인다. 계산작업은 수행하는 동안 계속 CPU 사용이 필요뿐더러, 싱글코어에서 멀티스레딩은 병렬로 동시에 처리되지 못하기 때문에 스레드간 전환이 발생하기 때문이다.

 

Multi Core에서 Single Thread와 Multi Thread의 비교

현제 실제 환경은 맥북 프로이므로 멀티코어에서의 실행 환경이다. 실행할 2개의 작업은 for 문으로 출력문을 각각 300번씩 문자를 출력하는 것이다. 수행시간이 너무 짧게 실행을 마치지 않도록, String 객체를 생성하여 출력한다. 이 2개의 작업을 싱글 스레딩과 멀티스레딩을 하여 결과를 확인해볼려고한다.

 

SingleThreadTest 클래스는 멀티 코어에서 싱글 스레드로 2개의 작업을 수행한다.

public class SingleThreadTest {  
    public static void main(String[] args) {  
        long startTime = System.currentTimeMillis();  
        for (int i = 0; i < 300; i++) {  
            System.out.printf("%s", new String("o"));  
        }  
        System.out.println("소요시간o:" + (System.currentTimeMillis() - startTime));  

        for (int i = 0; i < 300; i++) {  
            System.out.printf("%s", new String("x"));  
        }  
        System.out.println("소요시간x:" + (System.currentTimeMillis() - startTime));  
    }  
}

 

멀티 코어에서 하나의 스레드로 작업을 처리하면, 스케줄링에 따른 코어가 이동될수는 있어도 스레드는 한번에 하나의 코어에서만 실행된다. 그러므로 첫번째로 실행한 작업인 'o'를 먼저 출력하고 뒤 따라서 'x'를 출력한다. 2개의 작업을 처리하는데 총 소요시간은 62ms이다.

oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo소요시간o:34
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx소요시간x:62

 

MultiThreadTest 클래스는 멀티코어에서 두 개의 스레드로 2개의 작업 수행한다.

class MyThread extends Thread {  
    @Override  
    public void run() {  
        for (int i = 0; i < 300; i++) {  
            System.out.printf("%s", new String("x"));  
        }  
        System.out.println("소요시간x:" + (System.currentTimeMillis() - MultiThreadTest.startTime));  
    }  
}  

public class MultiThreadTest {  
    static long startTime = 0;  

    public static void main(String[] args) {  
        Thread t1 = new MyThread();  
        t1.start();  
        startTime = System.currentTimeMillis();  
        for (int i = 0; i < 300; i++) {  
            System.out.printf("%s", new String("o"));  
        }  
        System.out.println("소요시간o:" + (System.currentTimeMillis() - startTime));  
    }  
}

 

멀티 코어에서 두개의 스레드로 작업을 처리하면, 두개의 스레드는 동일한 코어에서 번갈아가면서 수행되거나 서로 다른 코어에서 동시에 수행된다. 그러므로 'o'와 'x'를 번갈아가면서 출력하게된다. 2개의 작업을 처리하는데 총 소요시간은 64ms이다.

oooooooooooooxxxxxxxxooooooxxxxxxxxxxooooooooooooooooooooxxxxxoooooooxxxxxxxxxooooooxxxxxxooooooooooxxxxxxxxxxxxxooooooooooooooooxxxxxxxxooooooooxxxxxxxxxxxxxxxxxxxxxxxxxxxxxoooooooooxxxoooooooooooooooooooooooooooxxooooooooooooooooooooooooooooooooooooooooooooooooooooooooxxxooooooxxxxxxxxxooooooxxxxxxxxxxoooooooxxxxxxxxxxxxxxxoooooooooxxxxxxxxxxxxxxxxxxxxxxxxooooooooooxxxxxxxxxxoooooooxxxxxxxxxxooooooooxxxxxxxxxoooooooxxxxxxxxooooooooxxxxxxxxxooooooxxxxxxxxooooooooooooooooooooooooooooooooooooooooooooooooxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx소요시간x:64
소요시간o:48

 

컴퓨터 실행환경이나 성능에 따라 실행결과는 달라질수 있지만, 실행결과를 비교해보도록하겠다. 멀티코어에서 싱글 스레딩이 멀티 스레딩보다 2ms더 빨랐다. 물론 시시각각 컴퓨팅 환경에 따라 다르겠지만, 싱글스레딩이 멀티 스레딩보다 빠른 경우가 생긴것이다.

 

위의 결과는 실행할때 마다 다른 결과를 얻을 수 있다. 매순간마다 컴퓨터 시스템의 상황을 고려하여 OS의 프로세스 스케줄러가 프로세스에 할당되는 실행시간이 일정하지 않고 스레드에게 할당되는 시간 역시 일정하지 않게된다. 그래서 스레드는 항상 이러한 불확실성을 가지고 있다는 것을 염두해야한다.

 

Java가 OS 독립적이라고 하지만, Java API는 OS library를 통해 파일 입출력, 네트워킹, 스레드 생성 및 관리 등을 제공받기 때문에, 이러한 부분은 OS에 종속적이다.

 

왼쪽이 멀티코어에서 싱글 스레드 그래프이고, 오른쪽이 멀티 스레드 그래프이다. 

 

두개의 스레드로 작업을 수행하는 것이 하나의 스레드로 작업을 수행하는 것보다 느린 이유는 다음 두가지 원인이 발생할수 있다.

  • 하나의 CPU 코어에서 두 스레드간에 전환이 되면서 시간 소요가 발생하였다.
  • 하나의 스레드가 화면(console)에 출력하고 있는 동안 다른 쓰레드는 출력이 끝나길 대기해야한다. 이때, 대기 시간이 발생한다.

Multi Core에서 스레드간 공유 자원 점유 경쟁

왼쪽 그래프를 보면, 멀티코어라도 하나의 코어에서 하나의 스레드만 실행되므로, 작업 A와 B는 순차대로 실행된다. 오른쪽 그래프를 보면, 멀티코어에서 두개의 스레드로 작업을 수행하면, 동시에 두개의 스레드가 수행될수 있으므로 A와 B 작업이 겹치는 부분이 생긴다. 그래서 console(출력) 자원을 두고 두개의 스레드가 경쟁하게 된다.

 

System.out.printf를 호출할 때, 실제로는 이 표준 출력 스트림을 통해 운영 체제의 파일 디스크립터와 상호작용하여 콘솔에 데이터를 출력한다. System.out은 PrintStream 객체로, 이는 표준 출력 스트림인 파일 디스크립터 1번(stdout)과 연결되어 있다. 그래서 이러한 출력 작업은 프로세스에서 열린 파일 자원을 스레드간에 공유하므로, 두개의 스레드가 동시에 실행되는 시점에 공유 자원에 접근해서 경쟁상태가 발생한다. 이시점에 스레드가 코어를 먼저 점유하게되면 표준 출력을 담당하는 파일에 lock이 걸리게된다. 이후, lock이 풀릴때까지 다른 스레드가 기달리게된다.

 

다음 코드는 PrintStream 클래스의 일부 코드이다. printf 메서드는 출력할 포맷과 출력물을 파라타매터로 전달받고 format 메서드를 호출하게된다.

public class PrintStream extends FilterOutputStream  
    implements Appendable, Closeable {

    private final InternalLock lock;

    public PrintStream printf(String format, Object ... args) {  
        return format(format, args);  
    }

    public PrintStream format(String format, Object ... args) {  
        try {  
            if (lock != null) {  
                lock.lock();  
                try {  
                    implFormat(format, args);  
                } finally {  
                    lock.unlock();  
                }  
            } else {  
                synchronized (this) {  
                    implFormat(format, args);  
                }  
            }  
        } catch (InterruptedIOException x) {  
            Thread.currentThread().interrupt();  
        } catch (IOException x) {  
            trouble = true;  
        }  
        return this;  
    }
}

 

Multi Thread에서 스레드간에 서로 다른 자원을 사용하는 경우

두 스레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글 스레드 프로세스보다 멀티스레드 프로세스가 더 효율적이다. 예를들어, 사용자로부터 데이터를 입력 받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와의 입출력을 필요로 하는 경우가 그렇다. 

 

만일, 사용자로 부터 입력받는 작업 A와 화면에 출력하는 작업 B를 하나의 스레드로 처리한다면, 왼쪽 그래프처럼 사용자가 입력할때까지 다른 작업을 수행하지 못하고 대기를 해야한다. 반면에, 두개의 스레드로 처리한다면 사용자 입력을 기다리는 동안에 다른 작업을 처리할수 있어서 보다 효율적으로 CPU를 쓸수있다. 따라서 작업 A와 B가 모두 종료되는 시간은 멀티 스레드 프로세스의 경우가 더 빨리 마치게된다.

다음은 단일 스레드로 사용자의 입력을 받는 작업과 화면에 숫자를 출력하는 작업을 처리하기 때문에, 사용자가 입력하기전까지 화면에 숫자가 출력되지 않는다.

import javax.swing.*;  

public class SingleThreadUserInputEx {  
    public static void main(String[] args) {  
        String input = JOptionPane.showInputDialog("Enter a sentence");  
        System.out.println("You entered: " + input);  

        for (int i=10; i > 0; i--) {  
            System.out.println(i);  
            try {  
                Thread.sleep(1000); // 1s  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}
You entered: hello
10
9
8
7
6
5
4
3
2
1

 

위와 달리 사용자로부터 입력받는 부분과 화면에 숫자를 출력하는 부분을 두개의 스레드로 나눠서 처리하여, 사용자가 입력을 마치지 않았어도 화면에 숫자가 출력되는 작업은 수행된다.

import javax.swing.*;  

class ThreadEx extends Thread{  
    public void run() {  
        for (int i=10; i > 0; i--) {  
            System.out.println(i);  
            try {  
                Thread.sleep(1000); // 1s  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  

public class MultiThreadUserInputEx {  
    public static void main(String[] args) {  
        ThreadEx thread = new ThreadEx();  
        thread.start();  

        String input = JOptionPane.showInputDialog("Enter a sentence");  
        System.out.println("You entered: " + input);  
    }  
}
10
9
8
7
6
5
You entered: hello
4
3
2
1

  • 참고
    • 자바의 정석 (남궁성 지음)