웹 브라우저를 실행하면 한 번에 여러 웹 페이지를 로드하고 사용자의 입력을 처리한다. 이러한 복잡한 작업을 가능하게 하는 것이 프로세스와 스레드이다.
프로세스와 스레드를 이해하고 구현함으로써 운영체제의 동작 원리를 이해하고, 효율적인 프로그램을 개발할 수 있다. 또한, 교착상태(프로세스나 스레드가 서로 필요한 자원을 점유하고 있어서 더 이상 진행할 수 없는 상태)와 같은 문제를 해결하기 위한 다양한 동기화 기법을 이해할 수 있다.
프로세스와 스레드 요약
프로세스(Process) | 스레드(Thread) |
운영체제로부터 자원을 할당받은 작업의 단위 | 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위 |
프로세스는 각각 독립된 메모리 공간을 가지며 실행되는 프로그램의 인스턴스이며, 스레드는 프로세스 내에서 실행되는 독립적인 작업 단위이다. 자바 등의 대부분의 프로그래밍 언어에서 멀티스레드 프로그래밍을 지원하고 있으며, 여러 작업을 동시에 처리하거나 병렬로 실행할 수 있다.
💡프로세스(Process)
프로세스는 운영체제로부터 실행에 필요한 자원을 할당받아 독립적으로 실행되는 프로그램의 인스턴스이다. 간단히 말하면 프로세스는 실행 중인 프로그램을 나타낸다.
프로세스는 메모리에 할당된 프로그램의 실행 환경을 포함하며, 프로그램 코드, 데이터, 스택, 힙 등의 자원을 관리한다.
작업관리자
Ctrl + Alt + Del 단축키를 누르면 나오는 작업 관리자에서 현재 실행 중인 프로세스와 해당 프로세스에 속한 스레드를 확인할 수 있다.
프로그램과 프로세스의 차이
프로그램 | 프로세스 | |
상태 | 저장 장치에 존재 | 메모리에 적재되고 실행 중 |
내용 | 코드와 데이터 집합 | 실행 중인 코드와 데이터 집합 |
실행 | 실행 가능한 파일 | 실제로 CPU에서 실행되고 있는 프로그램 |
상태 변화 | 실행되기 전 정적 상태 | 실행 중 동적 상태 변화 |
자원 할당 | 자원 할당되지 않음 | CPU 및 메모리 등의 자원 할당 |
예시 | 워드 문서, 웹 브라우저 등 | 워드 문서를 편집 중인 프로세스, 브라우저 작동 중인 프로세스 등 |
프로세스의 예시
public class MyProcess {
public static void main(String[] args) {
System.out.println("This is my Java process!");
}
}
위의 예시 코드는 자바를 사용하여 간단한 프로세스를 생성하는 방법이다. 이 프로세스는 메인 메서드를 통해 "This is my Java process!"라는 메시지를 출력한다.
왜 main에서 프로세스를 시작할까?
main이 프로세스를 시작하는 지점인 이유는 Java 프로그램이 실행될 때 JVM(Java Virtual Machine)이 프로그램을 시작하기 위해 main 메서드를 찾고, 이를 실행함으로써 프로세스가 시작되기 때문이다.
일반적으로 Java 프로그램은 여러 클래스로 구성될 수 있으나, JVM은 프로그램 실행을 시작할 때 반드시 main 메서드를 찾아 실행한다. 이 메서드는 다음과 같은 시그니처를 갖는다.
public static void main(String[] args)
따라서 main메서드가 프로세스의 시작점으로 정해진 것이다. 이 메서드를 통해 Java 프로그램이 실행되고, 이후 JVM이 다른 클래스와 메서드를 찾아가며 프로그램을 실행한다.
프로세스의 특징
1. 독립적인 메모리 공간을 가진다.
각 프로세스는 자체적인 메모리 공간을 가지며, 이는 다른 프로세스와 분리되어 있다. 따라서 한 프로세스의 데이터나 자원에 다른 프로세스가 직접적으로 접근할 수 없다.
2. 운영체제에 의한 관리된다.
프로세스는 운영체제에 의해 생성되고 관리된다. 운영체제는 각 프로세스에게 실행에 필요한 자원을 할당하고, 프로세스 간의 통신을 위한 메커니즘을 제공한다.
3. 하나 이상의 스레드를 보유하고 있다.
모든 프로세스는 최소한 하나의 스레드를 포함하며, 여러 개의 스레드를 가질 수 있다. 스레드는 프로세스 내에서 독립적으로 실행되는 작업 단위를 나타낸다.
프로세스의 자원 구조
프로세스는 할당된 메모리 자원을 활용하여 작업을 수행하며, 이때 각 메모리 영역을 코드 영역, 데이터 영역, 스택 영역, 힙 영역으로 구분한다.
스레드의 자원 공유
프로세스의 4가지 메모리 영역(Code, Data, Heap, Stack) 중 스레드는 스레드는 스택(Stack) 영역만을 할당받아 복사하고 나머지 영역은 프로세스 내의 다른 스레드들과 공유한다. 이는 각각의 스레드가 별도의 스택을 가지고 있지만, 코드, 데이터, 힙 메모리는 여러 스레드 간에 공유된다는 것을 의미한다. 따라서 각 스레드는 자신만의 스택을 가지고 있어서 독립적으로 함수 호출 및 지역 변수 등을 관리할 수 있지만, 힙 메모리는 공유되므로 서로 다른 스레드에서 힙 메모리를 읽고 쓸 수 있게 된다.
정적 영역과 동적 영역
프로세스의 자원 구조는 크게 두 가지로 나뉜다.
첫째로, 코드 영역과 데이터 영역은 프로그램이 실행될 때 결정되는 정적 영역이다.
코드 영역에는 프로그래머가 작성한 프로그램의 명령어들이 저장되며, 데이터 영역에는 프로그램이 실행되면서 사용하는 전역 변수나 상수들이 저장된다.
둘째로, 스택 영역과 힙 영역은 크기가 변하는 동적 영역이다.
스택 영역은 함수 호출 시 발생하는 지역 변수와 임시 데이터를 저장하는 공간으로, 함수의 호출과 함께 할당되며 함수의 종료 시 해제된다. 그리고 힙 영역은 사용자가 필요에 따라 동적으로 메모리를 할당하고 해제할 수 있는 영역이다. 주로 객체의 생성과 소멸이 이루어지는 영역으로, 필요한 만큼의 메모리를 할당하여 데이터를 저장한다.
코드 영역(Code / Text)
코드 영역은 프로그래머가 작성한 프로그램의 함수들이 CPU가 해석 가능한 기계어 형태로 저장되는 영역이다.
프로그램의 실행에 필요한 명령어가 저장되어 있다.
데이터 영역(Data)
데이터 영역에는 프로그램이 실행되면서 사용하는 전역 변수나 각종 데이터들이 모여 있는 영역이다. 이 영역은 .data, .rodata, .bss 영역으로 세분화된다.
- .data: 전역 변수나 static 변수 등 프로그램이 사용하는 데이터를 저장한다.
- .rodata: const로 선언된 상수 변수나 문자열 상수가 저장한다.
- .bss: 초기값이 없는 전역 변수나 static 변수가 저장한다.
힙 영역(Heap)
힙 영역은 동적으로 할당되는 데이터들을 저장하는 영역이다.
사용자가 메모리 공간을 동적으로 할당하고 해제할 수 있다. 주로 생성자, 인스턴스 등이 이 영역에서 동적으로 할당된다.
스택 영역(Stack)
스택 영역은 호출된 함수의 지역 변수와 함수 호출 시 발생하는 임시 데이터를 저장하는 영역이다.
함수의 호출과 함께 할당되며, 함수의 종료 시 소멸된다. 그리고 스택 영역이 초과하면 스택 오버플로우 에러가 발생한다.
멀티 프로세싱(Multi Processing)
멀티 프로세싱은 한 시스템에서 여러 개의 프로세서(CPU)를 사용하여 작업을 동시에 처리하는 것을 의미한다. 이는 각각의 프로세서가 독립적으로 작업을 처리하므로, 전체적인 처리 속도가 향상된다.
멀티 프로세싱은 멀티 코어(CPU)를 사용하여 여러 개의 작업을 병렬로 처리하거나, 여러 개의 프로세서가 하나의 작업을 처리하는 방식으로 구현될 수 있다.
멀티 프로세싱의 예시
멀티 프로세싱의 예시로는 멀티 코어 CPU를 사용하는 컴퓨터 시스템이나 클라우드 환경에서 여러 대의 서버가 동시에 작업을 처리하는 것을 들 수 있다.
멀티 프로세싱은 작업을 효율적으로 분산하여 처리할 수 있기 때문에, 대규모 시스템에서 높은 성능과 확장성을 제공하는 데 중요한 역할을 하며, 데이터베이스 서버, 웹 서버, 그래픽 처리 등 다양한 응용 프로그램에서 사용된다.
💡스레드(Thread)
스레드는 프로세스 내에서 실행되는 작업의 단위를 나타낸다. 한 프로세스 내에서 여러 스레드가 동시에 실행될 수 있으며, 각 스레드는 프로세스의 자원을 공유한다.
스레드를 사용하면 프로세스 내에서 병렬적으로 작업을 수행할 수 있다.
여러 스레드가 동시에 실행되는 것을 멀티 스레딩이라고 하며, 멀티 스레드 프로세스는 멀티 스레딩이 가능하다.
자바에서의 스레드
자바의 메인 메소드는 하나의 실행 흐름으로, 메인 스레드에 해당한다. 이는 main() 메서드에서 Thread.currentThread().getName()를 통해 확인할 수 있다.
스레드를 이용하면 하나의 프로세스에서도 병렬적으로 처리, 즉 여러 개의 처리 루틴을 가질 수 있다. 단순 반복의 코드를 실행할 때도 여러 개의 쓰레드를 만들어서 분리시킨 뒤 결과 데이터를 받아 합치면 그만큼 시간을 절약할 수 있다.
스레드의 예시
public class MyThread extends Thread {
public void run() {
System.out.println("This is my Java thread!");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
위의 코드에서는 MyThread 클래스가 Thread 클래스를 상속하여 스레드를 생성하고, run() 메서드를 오버라이드하여 스레드가 실행될 때 동작을 정의한다. main 메서드에서는 MyThread 클래스의 인스턴스를 생성하고 start() 메서드를 호출하여 스레드를 시작한다.
프로세스와 스레드는 서로 밀접한 관계를 가지고 있다. 하나의 프로세스는 최소한 하나의 스레드를 가지며, 멀티 스레드 프로세스는 여러 개의 스레드를 포함한다. 이러한 멀티 스레드 프로세스는 CPU의 여러 코어를 활용하여 병렬적으로 작업을 수행할 수 있다.
스레드의 특징
1. 프로세스 내에서 생성 및 실행된다.
스레드는 특정 프로세스 내에서 생성되고 실행된다. 각 프로세스는 최소한 하나의 스레드를 가지며, 멀티 스레드 프로세스라고 불리는 프로세스는 여러 개의 스레드를 포함한다.
2. 독립적인 실행 단위를 가진다.
각 스레드는 독립적으로 실행되며, 별도의 작업을 수행할 수 있다. 스레드 간에는 서로 영향을 미치지 않고 독립적으로 실행된다.
3. 자원을 공유한다.
스레드는 프로세스의 주소 공간을 공유하기 때문에 프로세스 내의 데이터를 공유하고 효율적으로 통신할 수 있다.
스레드의 생명주기
자바 쓰레드는 우선순위(Priority)와 라운드 로빈(Round Robin) 방식으로 스케줄링된다.
우선순위가 높은 스레드가 먼저 실행되며, 라운드 로빈 방식은 시간 할당량(Time Slice)을 정하여 스레드를 번갈아가며 실행한다.
- New: 스레드가 생성된 상태로 아직 start() 메소드가 호출되지 않은 상태이다.
- Runnable: start() 메소드가 호출되어 실행 대기 중인 상태로, Runnable pool에 모여있다.
- Wating: 일시 정지 상태로, 다른 스레드의 통지를 기다린다.
- Times Waiting: 주어진 시간 동안 일시 정지 상태로 대기한다.
- Block: 다른 스레드가 사용하고 있는 객체의 lock이 풀릴 때까지 대기한다.
- Terminated: 실행을 완료한 후 종료된 상태이다.
lang.Thread 주요 메소드
- start(): 스레드를 실행한다.
- run(): 스레드의 주요 작업을 정의한다.
- yield(): 실행 중인 스레드에게 실행 기회를 양보한다.
- sleep(): 현재 스레드를 주어진 시간 동안 일시 정지한다.
- join(): 다른 스레드의 작업이 완료될 때까지 대기한다.
- wait(): 스레드를 일시 정지 상태로 만든다.
- notify(): wait()로 인해 대기 중인 스레드를 실행 대기 상태로 전환한다.
- notifyAll(): 모든 대기 중인 스레드를 실행 대기 상태로 전환한다.
synchronized 키워드
synchronized 키워드는 뮤텍스(Mutex)와 세마포어(Semaphore)와 같은 개념으로, 스레드 간에 공유되는 자원의 접근을 동기화한다.
synchronized 키워드를 사용하여 메서드나 코드 블록을 동기화하면 해당 영역에는 한 번에 하나의 스레드만 접근할 수 있다.
데몬 스레드(Daemon Thread)
데몬 스레드는 백그라운드에서 동작하면서 일반 스레드의 작업을 돕는 보조적인 역할을 하는 스레드이다. 주로 일정한 작업을 주기적으로 수행하거나 다른 스레드의 지원을 위해 사용된다.
데몬 스레드는 일반 스레드의 작업이 모두 종료되면 자동으로 종료된다.
데몬 스레드의 특징
1. 백그라운드에서 동작한다.
데몬 스레드는 주로 백그라운드에서 동작하며, 일반 스레드의 작업을 지원하거나 보조적인 작업을 수행한다.
2. 자동으로 종료된다.
모든 일반 스레드가 종료되면 데몬 스레드는 자동으로 종료된다. 이는 데몬 스레드가 주 스레드의 작업이 완료되면 더 이상 필요하지 않다고 판단되는 경우에 유용하게 사용된다.
3. 일반 스레드를 지원한다.
데몬 스레드는 일반 스레드의 작업을 돕는 역할을 하기 때문에, 주로 주 스레드가 실행하는 작업을 지원하거나 보조하게 된다.
데몬 스레드의 예시
public class DaemonThreadExample {
public static void main(String[] args) {
MyDaemonThread daemonThread = new MyDaemonThread();
daemonThread.setDaemon(true); // 데몬 스레드로 설정
daemonThread.start(); // 스레드 시작
}
}
class MyDaemonThread extends Thread {
public void run() {
// 데몬 스레드가 수행할 작업을 정의
while (true) {
System.out.println("Daemon Thread is running...");
try {
Thread.sleep(1000); // 1초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
자바에서는 setDaemon(true) 메서드를 호출하여 스레드를 데몬 스레드로 지정할 수 있다.
위의 코드에서 MyDaemonThread 클래스는 Thread 클래스를 상속하여 데몬 스레드를 생성한다. run() 메서드에서는 데몬 스레드가 수행할 작업을 정의하고, 무한 루프를 통해 주기적으로 작업을 수행한다. main 메서드에서는 데몬 스레드를 생성한 후 setDaemon(true) 메서드를 호출하여 데몬 스레드로 지정한 후 시작한다.
멀티 스레딩(Multi Threading)
멀티 스레딩은 하나의 프로세스 내에서 여러 스레드가 동시에 실행되는 것을 의미한다. 이를 이용하여 여러 작업을 동시에 처리하거나 작업을 병렬적으로 수행할 수 있다.
멀티 스레딩은 CPU의 사용률을 향상하고 응답성을 향상하는 등의 장점을 가지고 있다.
멀티 스레딩을 사용하기 위해서는 스레드의 생성, 실행, 일시 중단, 종료 등의 작업을 적절히 관리해야 하므로, 스레드를 생성하고 관리하는 방법을 이해해야 한다. 또한 동시에 여러 스레드가 동일한 자원에 접근하면서 발생할 수 있는 문제를 방지하기 위해 동기화 메커니즘이 필요하며, 스레드 간의 동기화 문제에 유의해야 한다.
💡동기화
동기화는 멀티 스레드 환경에서 발생할 수 있는 스레드 간의 경쟁 상태(Race Condition)와 같은 문제를 해결하기 위한 기술이다. 여러 스레드가 공유된 자원에 접근할 때 발생할 수 있는 문제를 방지하기 위해 사용된다.
동기화가 필요한 이유
멀티 스레드 환경에서는 여러 스레드가 동시에 공유된 자원에 접근할 수 있다. 이때, 동일한 자원에 대해 동시에 여러 스레드가 변경을 시도할 경우 데이터의 일관성이 깨지고 예상치 못한 결과가 발생할 수 있다. 이러한 문제를 해결하기 위해 동기화가 필요하다.
동기화의 원리
1. 상호 배제(Mutual Exclusion)
한 스레드가 공유 자원을 사용하는 동안 다른 스레드는 해당 자원에 접근할 수 없도록 막는 것을 의미한다. 이를 통해 여러 스레드가 동시에 접근하지 못하도록 한다.
2. 임계 영역(Critical Section)
임계 영역은 공유 자원에 접근하는 코드 영역을 의미한다. 대표적으로 전역 변수나 heap 메모리 영역이 있다.
이 영역은 상호 배제에 의해 보호되어야 하며, 한 번에 한 스레드만 접근할 수 있도록 해야 한다.
동기화 방법
1. Locks(락)을 사용한다.
가장 일반적인 동기화 방법 중 하나로, 임계 영역에 진입하기 전에 락을 획득하고 나갈 때 락을 해제하는 방식이다. 자바에서는 synchronized 키워드를 사용하여 락을 활용할 수 있다.
2. 동기화 메서드 및 블록을 사용한다.
자바에서는 메서드나 특정 코드 블록을 동기화하여 사용할 수 있다. 이를 통해 해당 메서드나 블록에 대한 상호 배제를 구현할 수 있다.
동기화 예시
public class SynchronizationExample {
private static int count = 0;
private static final Object lock = new Object(); // 락 객체 생성
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) { // 락 획득
count++;
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) { // 락 획득
count++;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Count: " + count); // 결과 출력
}
}
위 코드는 자바에서 락을 사용하여 동기화하는 예시 코드로, synchronized 키워드를 사용하여 임계 영역을 지정하고, 락을 획득한 후에 작업을 수행한다. 이를 통해 여러 스레드가 count 변수에 안전하게 접근할 수 있다. 상호 배제를 통해 여러 스레드의 안전한 작업을 보장하고, 임계 영역을 정확하게 동기화하여 데이터의 일관성을 유지할 수 있다.
💡프로세스와 스레드의 사용
자바에서 프로세스와 스레드를 사용하여 작업을 동시에 처리하는 방법을 알아보도록 하자.
MainThread
public class MainThread {
public static void main(String[] args) {
// ThreadA 객체 생성
ThreadA threadA = new ThreadA();
// ThreadB 객체 생성
Thread threadB = new Thread(new ThreadB());
// ThreadA 시작
threadA.start();
// ThreadB 시작
threadB.start();
// 메인 스레드에서의 작업
for(int i = 0; i < 1000; i++) {
System.out.println("MainThread: " + i);
}
}
}
MainThread 클래스는 프로세스를 나타내며, main 메서드는 프로세스의 시작점이다.
이 클래스에서는 ThreadA, ThreadB 두 개의 스레드를 생성하고 시작한다.
메인 메서드에서 ThreadA와 ThreadB를 각각 다르게 선언한 이유는 다음과 같다.
ThreadA threadA = new ThreadA();에서는 ThreadA 클래스를 직접 사용하여 스레드를 생성한다. 이 경우 ThreadA 클래스의 run 메서드가 스레드로 실행된다.
반면에 Thread threadB = new Thread(new ThreadB());에서는 ThreadB 클래스가 Runnable 인터페이스를 구현하고 있기 때문에, 이를 생성자의 인자로 전달하여 새로운 스레드를 생성한다. 이 경우 ThreadB 클래스의 run 메서드가 스레드로 실행된다.
스레드의 생성 방법
1. extends Thread
Thread 클래스를 상속받아 run() 메서드를 오버라이딩하여 구현한다. 이후 객체를 생성하고 start() 메소드를 호출하여 스레드를 시작한다.
2. implements Runnable
Runnable 인터페이스를 구현하여 run() 메소드를 오버라이딩한다. 그리고 이를 구현한 객체를 Thread 클래스의 생성자에 전달하여 쓰레드를 생성하고 시작한다.
ThreadA
// ThreadA 클래스 정의
class ThreadA extends Thread {
@Override
public void run() {
for(int i = 0; i < 1000; i++) {
System.out.println("ThreadA: " + i);
}
}
}
ThreadA는 Thread 클래스를 직접 상속하고 run 메서드를 오버라이딩하여 스레드 동작을 정의한다.
이 방식은 상속을 사용하기 때문에 다른 클래스를 상속할 수 없고, 다중 상속이 불가능하다.
run 메서드를 재정의하여 스레드가 실행될 때 수행할 작업을 정의하며, 0부터 999까지의 숫자를 출력하는 작업을 수행한다.
ThreadB
// ThreadB 클래스 정의
class ThreadB implements Runnable {
@Override
public void run() {
for(int i = 0; i < 1000; i++) {
System.out.println("ThreadB: " + i);
}
}
}
ThreadB는 Runnable 인터페이스를 구현하는 방식으로 스레드를 생성한다.
이 방식은 다른 클래스를 상속받고 있을 때에도 사용할 수 있으며, 클래스가 이미 다른 클래스를 상속받고 있어도 Runnable을 구현할 수 있다. run 메서드를 재정의하여 스레드가 실행될 때 수행할 작업을 정의하며, 마찬가지로 0부터 999까지의 숫자를 출력하는 작업을 수행한다.
실행 결과
MainThread, ThreadA, ThreadB를 번갈아가면서 수행한다.
이처럼 프로세스와 스레드를 이용하여 작업을 동시에 처리할 수 있다.
참고 자료
[JAVA] 다중 작업(멀티 스레딩)을 위한 Thread 기초 개념, IT핥기, 2022.04.28.
[JAVA] 쓰레드(thread)와 프로세스(process), 멀티쓰레드, 멀티프로세싱, 데몬쓰레드(Daemon),동기화(Synchronized), choseongho, 2019.07.25.
멀티 프로세싱 vs 멀티 프로그래밍 vs 멀티 태스킹 vs 멀티 스레딩, Libi, 2021.07.07.
Thread의 모든 것! (스레드 생성, 생명주기, 정보, 상태, 스케줄링, 주요 메소드, synchronized), 빨간색소년, 2017.12.22.
완전히 정복하는 프로세스 vs 스레드 개념, Inpa, 2023.04.03.
멀티 태스킹 & 멀티 프로세싱 개념 한방 정리, Inpa, 2023.04.06.
스레드를 많이 쓸수록 항상 성능이 좋아질까?, Inpa, 2023.05.10.