Cloud + System/OS

프로세스 VS 쓰레드

vell_zero 2021. 10. 27. 11:45

https://devlog-wjdrbs96.tistory.com/145

 

[Java] 프로세스(process)와 쓰레드(Thread)의 개념과 구현

프로세스(process)란 간단히 말해서 '실행 중인 프로그램(program)'이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다. 그리고 프로그램을 수행하는 데 필

devlog-wjdrbs96.tistory.com

 

 

프로세스(process)란

간단히 말해서 '실행 중인 프로그램(program)'이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다. 그리고 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다. 

 

  • 프로세스 = 프로그램 + 프로세스제어블록 
  • 프로세스는 각각 독립된 메모리 영역(Code, Data, Stack, Heap의 구조)을 할당받는다.

 

출처 : https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html

 

 

2. 쓰레드(Thread)란? 

  • 스레드는 프로세스 내에서 각각 Stack만 따로 할당받고 Code, Data, Heap 영역은 공유한다.
  • 스레드는 한 프로세스 내에서 동작되는 여러 실행의 흐름으로, 프로세스 내의 주소 공간이나 자원들 (힙 공간 등)과 같은 프로세스 내에 스레드끼리 공유하면서 실행된다.
  • 같은 프로세스 안에 있는 여러 스레드들은 같은 힙 공간을 공유한다. 

 

출처 : https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html

 

 

3. 자바 스레드(Java Thread) 란

  • 일반 스레드와 거의 차이가 없으며, JVM가 운영체제의 역할을 한다.
  • 자바에는 프로세스가 존재하지 않고 스레드만 존재하며, 자바 스레드는 JVM에 의해 스케줄되는 실행 단위 코드 블록이다.

 

 

 

4. 자바에서의 쓰레드의 구현과 실행

쓰레드를 구현하는 방법은 Thread클래스를 상속받는 방법과 Runnable인터페이스를 구현하는 방법, 모두 두 가지가 있다. 어느 쪽을 선택해도 별 차이는 없지만 Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable인터페이스를 구현하는 방법이 일반적이다. 

 

 

1. Thread 클래스를 상속

1
2
3
4
5
6
class MyThread extends Thread {


    public void run() {
        작업내용          // Thread 클래스의 run()을 오버라이딩
    }
}

 

 

1 public class Thread implements Runnable  

 

Thread 클래스는 Runnable 인터페이스를 implements 하는 클래스이다. 

 

2. Runnable 인터페이스를 구현

1
2
3
4
5
6
7
8
public class Test2 implements Runnable {


    @Override
    public void run() {
        // 작업내용 
    }
}



 

 

Runnable 인터페이스는 아래와 같이 오로지 run()만 정의되어 있는 간단한 인터페이스이다. 

1
2
3
4
5
public interface Runnable {
   
    public abstract void run();
}



 

쓰레드를 구현한다는 것은 위의 두가지 방법 중에 한가지를 선택해서 run() 메소드의 몸통을 채워주면 된다.

 

 

3. Thread 예제 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ThreadEx1_1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5++i) {
            System.out.println(getName());
        }
    }
}


class ThreadEx2_2 implements Runnable {


    @Override
    public void run() {
        // Thread.currentThread() - 현재 실행중인 Thread를 반환한다. 
        System.out.println(Thread.currentThread().getName());
    }
}




public class Test2 {
    public static void main(String[] args) {
        ThreadEx1_1 t1 = new ThreadEx1_1();            // Thread의 자손 클래스의 인스턴스를 생성
        
        Runnable r = new ThreadEx1_1();                // Runnable을 구현한 클래스의 인스턴스를 생성
        Thread t2 = new Thread(r);                   // 생성자 Thread(Runnable target)
        
        Thread t3 = new Thread(new ThreadEx2_2());    // 위의 두줄을 한 줄로 간단히 
        
    }
}

 

 

위와 같이 Thread를 구현하기 위해서 2가지의 방법의 예시를 들었는데 대략적인 사용법은 감이 올 것이다.

그리고 15번째 줄을 보자. 만약에 Thread 클래스를 상속받으면 getName() 메소드(현재 실행중인 쓰레드 이름)을 호출할 수 있지만 Runnable 인터페이스를 구현한 클래스는 Thread클래스의 static 메소드만 호출할 수 있다. 

 

1
2
static Thread currentThread()   현재 실행중인 쓰레드의 참조를 반환한다
String getName()                 쓰레드의 이름을 반환한다.
 

 

따라서 currentThread().getName()을 이용해서 현재 실행중인 쓰레드의 이름을 출력해야 한다.

 

 

쓰레드의 실행 - start() 

 

 

그리고 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다는 것도 알아두어야 한다. 즉, 하나의 쓰레드에 대해 start()가 한번만 호출될 수 있다는 뜻이다. 

 

 

 

1. start()와 run() 

쓰레드를 실행시킬 때 run()이 아닌 start()를 호출한다는 것에 대해서 왜 그럴까? 라고 생각해봤을 것이다. 이제 한번 start()와 run()의 차이와 쓰레드가 실행되는 과정에 대해서 자세히 살펴보자.

 

모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에, 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸된다. 

 

 

  1. main 메소드에서 쓰레드의 start()를 호출한다.
  2. start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 생성한다.
  3. 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.
  4. 이제는 호출스택이 2개이므로 스케줄러가 정한 순서에 의해서 번갈아 가면서 실행된다.

 

 

main 쓰레드

main메소드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다. 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyThread extends Thread {


    public void run() {
        throwException();
    }


    public void throwException() {
        try {
            throw new Exception();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


public class Test {
    public static void main(String[] args) {
        MyThread my = new MyThread();
        my.start();
    }
}

 

 

  1. 처음에 main 쓰레드가 실행이 된다. 
  2. start()를 실행하고 main 쓰레드는 종료가 되면서 MyThread가 생성이 된다.
  3. 그리고 call stack에 run - throwException 순으로 쌓이게 된다.

 

새로 생성한 쓰레드에서 고의로 예외를 발생시키고 printStackTrace()를 이용해서 예외가 발생한 당시의 호출스택을 출력하는 예제이다. 호출스택의 첫 번째 메서드가 main메소드가 아니라 run메서드인 것을 확인하자 

따라서 main에서 에러가 발생하지 않는 결과창을 확인할 수 있다. 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyThread extends Thread {


    public void run() {
        throwException();
    }


    public void throwException() {
        try {
            throw new Exception();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


public class Test {
    public static void main(String[] args) {
        MyThread my = new MyThread();
        my.run();
    }
}

 

 

이번에는 start()가 아니라 run메소드를 실행해보는 예제이다. start()를 호출하지 않았기 때문에 새로운 쓰레드가 생성되지 않는다. 그저 그냥 오버라이딩한 run()가 호출이 된다. 따라서 여기서의 call stack의 첫번째 메소드는 main메소드이다.

따라서 main에서도 에러가 발생하는 것을 확인할 수 있다. 

 

 

start() 메소드의 내부

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public synchronized void start() {
      
        if (threadStatus != 0)
            throw new IllegalThreadStateException();


        group.add(this);


        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            
            }
        }
}

 

 

이거만을 봤을 때 start()를 실행했는데 왜 run메소드가 실행이 될까 생각을 해보니 내부적으로 호출하는 코드가 있는거 같다.

 

start0() 메소드와 Run 메소드

1
2
3
4
5
6
7
8
private native void start0();


    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
}