Thread 기본 개념에 대해서 알아보자
우리는 우리도 모르게 thread를 사용해왔다. 자바의 main 스레드를 실행시켜서 작업을 처리해왔다. 이런 스레드를 좀더 잘 이해하고 잘 사용하면 성능 향상을 만들어 낼수 있지만 공부해야할 것이 많은 그리고 작업을 만들때 고민해야하는 것이 많지만 확실히 도움이 될꺼라고 생각해서 공부하려 한다! 시리즈 식으로 정리하며 공부해보려고 한다.
1. 프로세스와 스레드
Process
프로세스는 메모리에서 실행중인 프로그램의 인스턴스를 뜻한다. 프로그램이 실행되면 운영체제가 메모리를 할당해서 프로세스를 생성하고 이를 통해서 프로그램이 독립적인 환경에서 실행된다.
- 독립성 : 각 프로세스는 독립적인 메모리 공간을 가지며 다른 프로세스가 접근할 수 없다.
- 자원 할당 : 운영체제는 프로세스마다 별도의 자원을 할당한다.
- 컨텍스트 스위칭 비용 : 프로세스 간 전환은 스레드보다 무겁고 비용이 많이 든다. 이유는 접은글 참조
- 다중 작업 : 여러 프로세스가 동시에 실행되면서 각자 독립적으로 작업을 수행할 수 있다.
- IPC : 프로세스 간 데이터를 주고 받기 위해 사용하는 통신기법을 필요로 한다.
Thread
프로세스 내에서 실행되는 작업의 흐름 단위이다. thread는 실을 꿰다 이런뜻인데 작업의 흐름이 실처럼 이어진다는 뜻이라고 한다. 하나의 프로세스에서는 여러개의 스레드를 가질수 있으며 이 스레드들은 자원을 공유하면서 병렬로 실행된다.
- 자원 공유 : 같은 프로레스 내의 스레드들을 메모리(heap)과 자원등을 공유해서 스레드 간의 데이터 교환이 용이하다.
- 컨텍스트 스위칭 비용 : 스레드는 프로세스에 비해서 비용이 적게든다. but 이는 이 비용을 무시하면 안된다.
- 동시성 처리 : 여러 스레드를 사용하면 동시성을 쉽게 구현할 수 있다.
- 동기화 문제 : 스레드는 자원을 공유하기 때문에 동기화 문제가 발생한다. 이를 통해서 교착 상태(데드락) 경쟁 상태 (race condition)이 발생할 수 있다.
컨텍스트 스위치 비용이 프로세스가 더 많이 드는 이유 :
- 메모리 공간 차이
프로세스는 독립적인 메모리 공간을 가지고 있으며 다른 프로세스로 변경할때 메모리 정보를 교환하는 과정에서 생기는 오버헤드 - 커널 오버헤드
프로세스 id, 상태, 레지스터, 메모리 매핑 등의 정보인 커널 정보를 교환해야한다. (PCB에 담겨 있다.)
같은 프로세스 안에 있는 스레드들은 자원들을 공유해서 컨텍스트 스위칭이 비교적 자원이 적게든다.
멀티프로세스
여러 프로세스를 생성해서 각각의 작업을 독립적으로 수행하는 방식, 프로세스 간 자원을 공유하지 않아 메모리 사용이 비효율적일수 있지만 안정성이 높다고 한다.
멀티스레드
하나의 프로세스 내에서 여러 스레드를 사용해 병렬로 작업을 처리하는 방식
Thread Pool
미리 생성된 스레드들의 집합으로 자바에서 동시성 작업을 효율적으로 처리하기 위해서 사용되는 개념이며 스레드 생성 및 제거로 인한 오버헤드를 줄이는 것이 목적이다.
스레드 수를 너무 많이 생성하게 되면 메모리 사용량이 너무 많이 증가할수 있고 CPU 스케줄링에 부담을 줄수 있다.
이에 대한것들은 다음에 다룰것 같으니 이번에는 패스 하겠다.
2. 간단한 스레드 생성 방법 (virtual thread 비교)
자바에서는 스레드를 생성하고 실행하는 다양한 방식을 제공하는데 자바 21에서는 virtual thread라는 os에서 thread를 가져오는거 대신 좀더 가벼운 방식으로 만들수 있다고 한다. 기존코드도 알아보고 가상도 알아보자 가상 스레드에 대해서는 나중에 제대로 정리하려하니 가볍게 봐줬으면 좋겠다.
2-1. thread 클래스
thread 클래스를 상속받고 run메서드를 오버라이드 해서 스레드가 실행할 작업을 정의하게 된다.
장점
간단하고 직관적이다.
단점
다중 상속이 불가능한 자바에서는 다른 클래스를 상속받고 있다면 thread를 상속받을수 없게되며, 스레드와 작업의 실행 로직이 강하게 결합되어 있기 때문에 다른 방식에 비해서 유연성이 부족하다.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("스레드 실행중");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 스레드 시작 (플랫폼 스레드)
}
}
기존 방식에서는 thread 클래스를 상속받아서 run()메서드를 override하고 start()메서드를 호출해서 스레드를 시작할 수 있다. 이는 OS에서 관리하는 플랫폼 스레드를 사용해서 비용이 많이 들수 있다.
public class Main {
public static void main(String[] args) {
Thread.startVirtualThread(() -> {
System.out.println("스레드 실행중");
});
}
}
좀더 가볍게 스레드를 생성할 수 있다. 이 thread는 JVM에서 관리되기 때문에 OS스레드에 비해서 매우 가볍다.
startVirtualThread()메서드를 통해서 가상 스레드를 생성할 수 있다.
2-2. runnalbe 인터페이스
인터페이스를 상속받아서 스레드를 생성하는 방식이다.
장점
스레드와 실행 로직이 분리됨으로 더 유연하게 사용할수 있다. 실행로직을 별도 클래스에 구현하고 필요한 곳에서 스레드를 생성할수 있다. 또한 인터페이스를 상속받는 만큼 다중 상속을 받을수 있다.
단점
스레드 객체를 추가로 생성해야 해 코드가 복잡해 질수 있다.
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("스레드 실행중");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 스레드 시작 (플랫폼 스레드)
}
}
Runnable 인터페이스를 구현한후 이를 thread에 전달해서 스레드를 실행할 수 있다.
public class Main {
public static void main(String[] args) {
Runnable task = () -> System.out.println("스레드 실행중");
Thread.startVirtualThread(task); // 가상 스레드에서 Runnable 실행
}
}
기존의 Runnable을 사용하는 코드에서 가상 스레드를 사용할 수 있게 됨으로써 큰 수정 없이 코드 성능을 극대화할 수 있습니다.
2-3. executor framework 소개
스레드 풀을 통해서 스레드를 관리하는 방식이다. 스레드가 부족해지면 자동으로 생성하고 작업이 끝나면 다시 스레드를 재활용한다.
장점
스레드를 풀에서 재활용함으로써 새로운 스레드를 반복해서 생성할 필요 없다. 이는 대규모 작업 처리에 적합하며 스레드 관리 부담을 줄여준다.
단점
스레드 풀을 설정하고 관리하는 과정이 상대적으로 복잡하다. 스레드 풀의 크기를 잘못 설정하면, 스레드가 부족하거나 불필요하게 많이 생성될 수 있다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> System.out.println("스레드 풀 실행중"));
executor.shutdown();
}
}
기존 자베에서는 Excutors를 통해서 스레드 풀을 관리할 수 있다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> System.out.println("스레드 풀 실행중"));
executor.shutdown();
}
}
가상 스레드를 사용할수 있는 Executor Framework를 제공한다. 이를 통해서 스레드 풀을 관리할 수 있다.
Coding, Software, Computer Science 내가 공부한 것들 잘 이해했는지, 설명할 수 있는지 적는 공간