자바의 정석- 프로세스와 쓰레드

Sangwon Na·2021년 9월 23일
0
post-thumbnail

1. 프로세스와 쓰레드

프로세스란 실행 중인 프로그램 이다.

프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.

  • 프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다. 그래서 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 멀티쓰레드 프로세스라고 한다.
  • 쓰레드를 프로세스라는 작업공간에서 작업을 처리하는 일꾼으로 생각하면 이해하기 쉬울 것이다.
  • 하나의 프로세스가 가질 수 있는 쓰레드의 개수는 제한되어 있지 않으나 쓰레드가 작업을 수행하는데 개별적인 메모리공간(호출스택)을 필요로 하기 때문에 프로세스의 메모리 한계(호출스택의 크기)에 따라 생성할 수 있는 쓰레드의 수가 결정된다.

⚽ 멀티쓰레딩의 장점
1. CPU의 사용률을 향상시킨다.
2. 자원을 보다 효율적으로 사용할 수 있다.
3. 사용자에 대한 응답성이 향상된다.
4. 작업이 분리되어 코드가 간결해진다.

⚽ 메신저의 경우(예시)
채팅하면서 파일을 다운로드 받거나 음성대화를 나눌 수 있는 것이 가능한 이유가 바로 멀티쓰레드로 작성되어 있기 때문이다. 만일 싱글쓰레드로 작성되어 있다면 파일을 다운로드 받는 동안에는 채팅을 할 수 없을 것이다.

⚽ 단점
멀티쓰레드 프로세스는 여러 쓰레드가 같은 프로세스 내에세 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화(Synchronization), 교착상태(deadlock)와 같은 문제들을 고려해서 신중히 프로그래밍해야 한다.
*교착상태란 두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태를 말한다.

2. 쓰레드의 구현과 실행

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

Runnable인터페이스를 구현하는 방법이 객체지향적인 방법!

1. Thread클래스를 상속

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



2. Runnable인터페이스를 구현

class MyThread implements Runnable {
	public void run() { /* 작업내용 */ }
    // Runnable 인터페이스의 추상메서드 run()을 구현
}
  • 쓰레드를 구현한다는 것은 run()의 몸통을 채우기만 하면 된다.

Runnable인터페이스를 구현한 경우, Runnable인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 가지고 Thread클래스의 인스턴스를 생성할 때 생성자의 매개변수로 제공해야 한다.

Thread클래스를 상속받으면, Thread클래스의 메서드를 직접 호출할 수 있지만, Runnable을 구현하면 Thread클래스의 static 메서드인 currentThread()을 호출하여 쓰레드에 대한 참조를 얻어 와야만 호출이 가능하기 때문에, Thread.currentThread().getName()와 같이 클래스명까지 적어 참조해야 한다.

  • 쓰레드의 이름은 다음과 같은 생성자나 메서드를 통해서 지정 또는 변경할 수 있다.
Thread(Runnable target, String name)
Thread(String name)
void setName(String name)

쓰레드의 이름을 지정하지 않으면 'Thread-번호'의 형식으로 이름이 정해진다.
쓰레드를 생성한 다음에는 start()를 호출해야만 비로소 작업을 시작한다.

❓ 한 번 사용한 쓰레드는 다시 재사용할 수 없다.
즉, 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다는 뜻이다.

  • 하나의 쓰레드에 대해 start()를 두 번 이상 호출하면 실행시에 IllegalThreadStateException이 발생한다.

  • 한 번 더 수행되기를 원한다면 새로운 쓰레드를 생성한 다음에 start()를 호출해야 한다.

❓ start()가 호출된 쓰레드는 바로 실행되는 것이 아니라는 것에 주의해야한다. 일단 대기상태로 있다가 스케줄러가 정한 순서에 의해서 실행된다.

3. start()와 run()

쓰레드를 실행시킬 때 start()와 run()에 대한 차이와 쓰레드가 실행되는 과정에 대해서 설명하고자 한다.

프로그램이 실행하면 기본적으로 하나의 쓰레드(일꾼)을 생성하고, 그 쓰레드가 main메서드를 호출해서 작업이 수행되도록 한다.

run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 속한 메소드 하나를 호출하는 것이다.

start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 저장되게 한다. 모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에, 새로운 쓰레드를 생성하고 실행시킬 때마다 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸된다.

  1. main메서드에서 쓰레드의 start메서드를 호출한다.
  2. start메서드는 쓰레드가 작업을 수행하는데 사용될 새로운 호출스택을 생성한다.
  3. 생성된 호출스택에 run메서드를 호출해서 쓰레드가 작업을 수행하도록 한다.
  4. 호출스택이 2개이기 때문에, 스케줄러가 정한 순서에 의해서 번갈아 가면서 실행된다.

start()으로 실행 시


새로 생성한 쓰레드에서 고의로 예외를 발생시키고 printStackTrace()를 이용해서 예외가 발생한 당시의 호출스택을 출력하는 예제이다. 호출스택의 첫번째 메서드가 main메서드가 아니라 run메서드인 것을 확인할 수 있다.
❓ 한 쓰레드에서 예외가 발생해서 종료되어도 다른 쓰레드의 실행에는 영향을 미치지 않는다.


run()으로 실행 시

main메서드가 호출스택에 포함되어 있음을 확인 할 수 있다.

4. 싱글쓰레드와 멀티쓰레드


하나의 쓰레드로 두 작업을 처리하는 경우는 한 작업을 마친 후에 다른 작업을 시작하지만, 두 개의 쓰레드로 작업 하는 경우에는 짧은 시간동안 2개의 쓰레드(th1, th2)가 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는 것과 같이 느끼게 한다. 오히려 두 개의 쓰레드로 작업한 시간이 싱글쓰레드로 작업한 시간보다 더 걸릴 수 있는데, 그 이유는 쓰레드간의 작업전환(context switching)에 시간이 걸리기 때문이다. 단순히 CPU만을 사용하는 계산작업이라면 오히려 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 더 효율적이다.
❓ 프로세스간 또는 쓰레드간의 전환을 컨텍스트 스위칭 이라고 한다.


싱글쓰레드


멀티쓰레드(2개 이상의 쓰레드가 존재하면)

  • 실행할 때마다 다른 결과를 얻을 수 있는데, 그 이유는 실행 중인 프로그램(프로세스)이 OS의 프로세스 스케줄러의 영향을 받기 때문이다. 따라서 실행순서와 실행시간이 일정하지 않고 쓰레드에게 할당되는 시간 역시 일정하지 않는 불확실성을 가지고 있다는 것을 염두에 두어야 한다.

멀티쓰레드의 장점은, 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와의 입출력을 필요로 하는 경우가 이에 해당된다.

5. 쓰레드의 우선순위


쓰레드는 우선순위라는 속성(멤버변수)을 가지고 있는데, 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다. 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.

void setPriority(int newPriority): 쓰레드의 우선순위를 지정한 값으로 변경
int getPriority(): 쓰레드의 우선순위를 반환한다.

public static final int MAX_PRIORITY = 10 // 최대우선순위
public static final int MIN_PRIORITY = 1 // 최소우선순위
public static final int NORM_PRIORITY = 5 // 보통우선순위

쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을수록 우선순위가 높다. 그러나 우선순위의 높고 낮음은 절대적인 것이 아니라 상대적이다.
예를 들어 프로세스의 두 쓰레드의 우선순위를 각각 1과 2로 설정하는 것과 9와 10으로 설정하는 것은 같은 결과를 얻는다. 즉 비율로 나누어 할당할 것인지에 따라 우선순위 차이가 결정된다.

쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다. main 메서드를 수행하는 쓰레드는 우선순위가 5이므로, main메서드 내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다.

6. 쓰레드 그룹

쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 것으로, 폴더를 생성해서 관련된 파일들을 함께 넣어서 관리하는 것처럼 쓰레드 그룹을 생성해서 쓰레드를 그룹으로 묶어서 관리할 수 있다.

ThreadGroup을 사용해서 생성할 수 있으며, 주요 생성자와 메서드는 다음과 같다.

ThreadGroup(String name) : 지정된 이름의 새로운 쓰레드 그룹을 생성한다.
ThreadGroup(ThreadGroup parent, String name) : 지정된 쓰레드 그룹에 포함되는 새로운 쓰레드 그룹을 생성한다.

쓰레드를 쓰레드 그룹에 포함시키려면 Thread의 생성자를 이용해야 한다.

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하기 때문에, 위와 같이 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다.

❓ 자바 어플리케이션이 실행되면, JVM은 main과 system이라는 쓰레드 그룹을 만들고 JVM운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킨다. main은 main이라는 이름의 쓰레드, 가비지컬렉션을 수행하는 Finalizer쓰레드는 system 쓰레드 그룹에 속한다.

따라서 우리가 생성하는 모든 쓰레드 그룹은 main쓰레드 그룹의 하위 쓰레드 그룹이 되며, 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동적으로 main쓰레드 그룹에 속하게 된다.

7. 데몬쓰레드

데몬 쓰레드는 다른 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다. 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동종료되는데, 그 이유는 데몬 쓰레드는 일반 쓰레드의 보조역할을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재의 의미가 없기 때문이다.

데몬 쓰레드의 예로는 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신 등이 있다.

3초마다 변수 autoSave의 값을 확인해서 그 값이 true이면, autoSave()를 호출하는 일을 무한히 반복하도록 쓰레드를 작성하였다. 만일 이 쓰레드를 t.setDaemon(true)로 데몬 쓰레드로 설정하지 않았다면, 이 프로그램은 강제종료하지 않는 한 영원히 종료되지 않는다.

❓ main메소드를 쓰레드(프로그램)을 실행하면, JVM은 가비지컬렉션, 이벤트처리, 그래픽처리와 같이 프로그램이 실행되는 데 필요한 보조작업을 수행하는 데몬 쓰레드들을 자동적으로 생성해서 실행시킨다.

profile
나상원의 LOG

0개의 댓글