Java/Java

[Java] 싱글 쓰레드와 멀티 쓰레드

웅지니어링 2022. 12. 30. 15:31

* 쓰레드와 프로세스

프로세스란 간단히 말해서 '실행 중인 프로그램'이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다. 프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다. 그래서 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 '멀티 쓰레드 프로세스'라고 한다.

 

* 멀티 쓰레딩의 장점

1. CPU의 사용률을 향상시킨다.

2. 자원을 보다 효율적으로 사용할 수 있다.

3. 사용자에 대한 응답성이 향상된다.

4. 작업이 분리되어 코드가 간결해진다.

 

* 싱글 쓰레드와 멀티 쓰레드

두 개의 작업을 하나의 쓰레드(th1)로 처리하는 경우와 두 개의 쓰레드(th1, th2)로 처리하는 경우를 가정해보자. 하나의 쓰레드로 두 작업을 처리하는 경우는 한 작업을 마친 후에 다른 작업을 시작하지만, 두 개의 쓰레드로 작업하는 경우에는 짧은 시간동안 2개의 쓰레드(th1, th2)가 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는 것과 같이 느끼게 한다.

위의 그래프에서 알 수 있듯이 하나의 쓰레드로 두 개의 작업을 수행한 시간과 두 개의 쓰레드로 두 개의 작업을 수행한 시간은 거의 같다. 오히려 두 개의 쓰레드로 작업한 시간이 싱글 쓰레드로 작업한 시간보다 더 걸리게 되는데 그 이유는 쓰레드 간의 작업 전환(Context Switching)에 시간이 걸리기 때문이다. 작업 전환을 할 때는 현재 진행 중인 작업의 상태, 예를 들면 다음에 실행해야 할 위치(PC, 프로그램 카운터) 등의 정보를 저장하고 읽어 오는 시간이 소요된다. 참고로 쓰레드의 스위칭에 비해 프로세스의 스위칭이 더 많은 정보를 저장해야 하므로 더 많은 시간이 소요된다. 따라서 싱글 코어에서 단순히 CPU만을 사용하는 계산 작업이라면 오히려 멀티 쓰레드보다 싱글 쓰레드로 프로그래밍하는 것이 더 효율적이다.

 

* 쓰레드의 I/O 블로킹(blocking)

두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글 쓰레드 프로세스보다 멀티 쓰레드 프로세스가 더 효율적이다. 예를 들면 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와의 입출력을 필요로 하는 경우가 이에 해당한다. 만일 사용자로부터 입력받는 작업(A)과 화면에 출력하는 작업(B)을 하나의 쓰레드로 처리한다면 사용자가 입력을 마칠 때까지 아무 일도 하지 못하고 기다리기만 해야 한다. 그러나 두 개의 쓰레드로 처리한다면 사용자의 입력을 기다리는 동안 다른 쓰레드가 작업을 처리할 수 있기 때문에 보다 효율적인 CPU의 사용이 가능하다. 작업 A와 B가 모두 종료되는 시간을 비교하면 멀티 쓰레드 프로세스의 경우가 작업을 더 빨리 마치는 것을 알 수 있다. 쓰레드가 입출력(I/O) 처리를 위해 기다리는 것을 I/O 블로킹이라고 한다.

 

* 쓰레드의 동기화(synchronization)

싱글 쓰레드 프로세스의 경우 프로세스 내에서 단 하나의 쓰레드만 작업하기 때문에 프로세스의 자원을 가지고 작업하는데 별 문제가 없지만, 멀티 쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다. 만일 쓰레드 A가 작업하던 도중에 다른 쓰레드 B에게 제어권이 넘어갔을 때, 쓰레드 A가 작업하던 공유 데이터를 쓰레드 B가 임의로 변경하였다면, 다시 쓰레드 A가 제어권을 받아서 나머지 작업을 마쳤을 때 원래 의도했던 것과는 다른 결과를 얻을 수 있다. 이러한 일이 발생하는 것을 방지하기 위해서 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 임계 영역(Critical section)잠금(Lock)이다. 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다. 그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다. 이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 '쓰레드의 동기화(Synchronization)'라고 한다.

 

* Java로 구현한 동기화

public class Main {
    public static void main(String[] args) {
        Runnable r = new RunnableEx();
        new Thread(r).start();
        new Thread(r).start(); // ThreadGroup에 의해 참조되므로 gc 대상이 아니다.
    }
}

class Account {
    private int balance = 1000; // private으로 해야 동기화의 의미가 있다.

    public int getBalance() {
        return balance;
    }

    public synchronized void withDraw(int money) {
        if(balance >= money) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            balance -= money;
        }
    }
}

class RunnableEx implements Runnable {
    Account acc = new Account();

    public void run() {
        while(acc.getBalance() > 0) {
            // 100, 200, 300 중의 한 값을 임의로 선택해서 출금(withDraw)
            int money = (int)(Math.random() * 3 + 1) * 100;
            acc.withDraw(money);
            System.out.println("balance : " + acc.getBalance());
        }
    }
}

위의 예제에서 withDraw()에 synchronized를 붙이지 않는다면 동기화가 진행되지 않는다.

위의 실행 결과는 동기화를 진행하지 않았을 경우이다. 실행 결과를 보면 잔고(balance)가 음수가 되는 것을 볼 수 있는데, 그 이유는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다.

 

위의 실행 결과는 동기화를 진행했을 경우이다. 전과 달리 결과에 음수 값이 나타나지 않는 것을 확인할 수 있다. 한 가지 주의할 점은 Account 클래스의 인스턴스 변수인 balance의 접근 제어자가 private라는 것이다. 만일 private이 아니면, 외부에서 직접 접근할 수 있기 때문에 아무리 동기화를 해도 이 값의 변경을 막을 길이 없다. synchronized를 이용한 동기화는 지정된 영역의 코드를 한 번에 하나의 쓰레드가 수행하는 것을 보장하는 것일 뿐이기 때문이다.

'Java > Java' 카테고리의 다른 글

[Java] Enum 클래스  (0) 2023.11.16
[Java] 오버로딩 / 오버라이딩 정리  (0) 2023.02.08
[Java] Garbage Collection(GC)의 개념과 동작 원리  (0) 2023.01.13
[Java] Math.random 메서드  (0) 2022.12.24