티스토리 뷰

ALL/Java

[Java] Thread

whoAmI_ 2023. 11. 27. 17:09

 

자바 스레드를 공부하는데 관련 설명들을 보는데 프로세스, 스레드, 멀티태스킹, 멀티 프로세싱, 멀티 스레딩 등 너무 헷갈리는 용어들이 많았다. 먼저 알아야 하는 용어부터 짚고 가겠다.

 

프로세스(process)

‘실행 중인 프로그램(program)’

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

프로세스 = 프로그램을 수행하는데 필요한 자원(데이터와 메모리 등) + 쓰레드 로 구성

프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드 이다.

 

쓰레드(Thread)

프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드 이다.

모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재.

둘 이상의 쓰레드를 가진 프로세스를 ‘멀티쓰레드 프로세스(multi-threaded process)’라고 한다.

 

멀티태스킹

윈도우나 유닉스를 포함한 대부분의 OS는 멀티태스킹(multi-tasking, 다중작업)을 지원하기 때문에 여러개의 프로세스가 동시에 실행될 수 있다.

 

멀티쓰레딩

하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것

CPU의 코어(core)가 한 번에 단 하나의 작업만 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 개수는 코어의 개수와 일치한다.

처리해야하는 쓰레드의 수는 언제나 코어의 개수보다 훨씬 많기 때문에 각 코어가 아주 짧은 시간동안 여러 작업을 번갈아 가며 수행함으로써 여러 작업들이 모두 동시에 수행되는 것처럼 보이게 한다.

그래서 프로세스의 성능이 단순히 쓰레드의 개수에 비례하는 것은 아니다.

장점

  • CPU의 사용률 향상
  • 자원의 효율적 사용
  • 사용자에 대한 응답성 향상
  • 작업 분리, 간결한 코드 가능

여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티쓰레드로 작성하는 것은 필수적이며, 하나의 서버 프로세스가 여러개의 쓰레드를 생성해서 쓰레드와 사용자의 요청이 일대일로 처리되도록 프로그래밍해야 한다.

단점

멀티쓰레드 프로세스는 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화(synchronization), 교착상태(deadlock)와 같은 문제들을 고려해야 한다. 

 


 

쓰레드 구현 방법

  1. Thread 클래스를 상속
class MyThread extends Thread {
	pulic void run() { /* 작업내용 */ } // Thread클래스의 run()을 오버라이딩
}
  1. ⭐ Runnable 인터페이스를 구현
  2. Runnable인터페이스는 오로지 run()만 정의되어 있는 간단한 인터페이스이다.
class MyThread implements Runnable {
	public void run() { /* 작업내용 */ } // Runnable 인터페이스의 run()을 구현
}

Thread 클래스를 상속받은 경우와 Runnable인터페이스를 구현한 경우의 인스턴스 생성 방법이 다르다.

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

실제 Thread클래스의 소스코드를 이해하기 쉽게 수정해놓은 것을 보면, 인스턴스변수로 Runnable타입의 변수를 선언해놓고, 생성자를 통해서 Runnable인터페이스를 구현한 인스턴스를 참조하도록 되어있는 것을 확인할 수 있다.

그리고 run()을 호출하면 참조변수를 통해 Runnable인터페이스를 구현한 인스턴스의 run()이 호출된다.

이렇게 함으로써 상속을 통해 run()을 오버라이딩하지 않고도 외부로부터 run()을 제공받을 수 있게 된다.

public class Thread {
	private Runnable r; //Runnable 인터페이스를 구현한 클래스의 인스턴스를 참조하기 위한 변수
	
	public Thread(Runnable r){
		this.r = r;
	}
	public void run() {
		if(r!=null){
			r.run(); //Runnable 인터페이스를 구현한 인스턴스의 run()을 호출
		}
	}
}

 

쓰레드의 실행 - start()

쓰레드를 생성했다고 해서 자동으로 실행되는 것은 아니다. start()를 호출해야만 쓰레드가 실행된다.

💡 사실은 start()가 호출되었다고 바로 실행되는 것이 아니라, 일단 실행 대기 상태에 있다가 자신의 차례가 되어야 실행된다. 물론 실행 대기중인 쓰레드가 하나도 없으면 곧바로 실행 상태가 된다.

참고

쓰레드의 실행 순서는 OS의 스케줄러가 작성한 스케줄에 의해 결정된다.

⭐ 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다. 즉, 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다는 뜻이다.❗

 

start() 와 run()

start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 올라가게 한다.

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

일반적으로 호출스택에서는 가장 위에 있는 메서드가 현재 실행중인 메서드, 나머지 메서드들은 대기상태에 있다.

그런데 쓰레드가 둘 이상인 경우, 호출스택 최상위에 있는 메서드일지라도 대기상태에 잇을 수 있다.

스케줄러는 실행대기중인 쓰레드들의 우선순위를 고려하여 실행순서와 실행시간을 결정하고, 각 쓰레드들은 작성된 스케줄에 따라 자신의 순서가 되면 지정된 시간동안 작업을 수행한다.

 

main쓰레드

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

우리는 우리도 모르는 사이 지금까지 이미 쓰레드를 사용하고 있었던 것이다. 앞서 쓰레드가 일꾼이라고 했는데, 프로그램이 실행되기 위해서 작업을 수행하는 일꾼이 최소한 하나는 필요하다. 그래서 프로그램을 실행하면 기본적으로 하나의 쓰레드(일꾼)를 생성하고, 그 쓰레드가 main메서드를 호출해서 작업이 수행되도록 하는 것이다.

지금까지는 main메서드가 수행을 마치면 프로그램이 종료되었으나, 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다.

 💡 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.

쓰레드는 ‘사용자 쓰레드(user thread)’와 ‘데몬 쓰레드(daemon thread)’, 두 종류가 있다.

 

Single thread VS Multi thread

싱글코어 인 경우

  1. 하나의 쓰레드로 두 개의 작업을 수행하는 경우

       한 작업을 마친 후에 다른 작업을 시작

    2. 두 개의 쓰레드로 두 개의 작업을 수행하는 경우

        짧은 시간동안 2개의 쓰레드를 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는 것과 같이 보임

 

작업 수행 시간 비교

싱글코어 인 경우

하나의 쓰레드로 두 개의 작업을 수행하는 경우  <  두 개의 쓰레드로 두 개의 작업을 수행하는 경우

why?

⇒ 쓰레드간의 작업전환(context switching)에 시간이 걸림

작업 전환할 때 현재 진행 중인 작업의 상태(다음에 실행해야 할 위치(PC, 프로그램 카운터))등의 정보를 저장하고 읽어오는 시간이 소요.

참고로 쓰레드의 스위칭에 비해 프로세스의 스위칭이 더 많은 정보를 저장해야 하므로 더 많은 시간이 소요된다.

참고

프로세스 또는 쓰레드 간의 작업전환을 컨텍스트 스위칭(context switching)이라고 한다.

그래서 싱글코어에서 단순히 CPU만 사용하는 계산작업이라면 오히려 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 더 효율적이다.

멀티코어인 경우

싱글 코어로 두개의 쓰레드를 실행하는 경우 vs 멀티 코어로 두 개의 쓰레드를 실행하는 경우

싱글코어인 경우 : 멀티쓰레드라도 하나의 코어가 번갈아가면서 작업을 수행하는 것이므로, 두 작업이 절대 겹치지 않는다.

그러나!

멀티코어인 경우 : 멀티쓰레드로 두 작업을 수행하면, 동시에 두 쓰레드가 수행될 수 있으므로 두 작업이 겹치는 부분이 발생한다. 그래서 화면(console)이라는 자원을 놓고 두 쓰레드가 경쟁하게 되는 것이다.

참고

여러 쓰레드가 여러 작업을 동시에 진행하는 것을 병행(concurrent)라고 하고, 하나의 작업을 여러 쓰레드가 나눠서 처리하는 것을 병렬(parallel)이라고 한다.

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

 

쓰레드의 우선순위

쓰레드는 우선순위(priority)라는 속성(멤버변수)을 갖고있는데, 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다. 한가지 알아두어야 할 점은 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다는 것이다. main메서드를 수행하는 쓰레드는 우선순위가 5이므로 main메서드 내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다.

싱글 코어로 두 개의 쓰레드로 두 개의 작업을 실행했을 때,

우선순위가 같은 경우 각 쓰레드에게 거의 같은 양의 실행시간이 주어지지만, 우선순위가 다르다면 우선순위가 높은 스레드에게 상대적으로 보다 더 많은 양의 실행시간이 주어지고 결과적으로 작업이 더 빨리 완료될 수 있다.

하지만,

멀티 코어로 두 개의 쓰래드로 두 개의 작업을 실행했을 때,

 💡 멀티코어에서는 쓰레드의 우선순위에 따른 차이가 거의 아니 전혀 없었다.

 

결국 우선순위에 차등을 두어 쓰레드를 실행시키는 것이 별 효과가 없었다.

그저 쓰레드에 높은 우선순위를 주면 더 많은 실행시간과 실행기회를 갖게 될 것이라고 기대할 수는 없는 것이다.

멀티코어라 해도 OS마다 다른 방식으로 스케줄링 하기 때문에,

굳이 우선순위에 차등을 두어 쓰레드를 실행하려면, 특정 OS의 스케줄링 정책과 JVM의 구현을 직접 확인해봐야 한다.

자바는 쓰레드가 우선순위에 따라 어떻게 다르게 처리되어야 하는지에 대에 강제하지 않으므로, 쓰레드의 우선 순위와 관련된 구현이 JVM마다 다를 수 있기 때문이다.

만일 확인한다 하더라도 OS스케줄러에 종속적이라서 어느정도 예측만 가능한 정도일 뿐 정확히 알 수 없다.

💡 따라서, 차라리 쓰레드에 우선순위를 부여하는 대신 작업에 우선순위를 두어 PriorityQueue에 저장해 놓고, 우선순위가 높은 작업이 먼저 처리되도록 하는 것이 나을 수 있다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
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
글 보관함