본문 바로가기

Java

[Java] 이것이 자바다 - 멀티 프로세스, 멀티 스레드

운영체제

운영체제란 하드웨어와 소프트웨어를 관리하는 일꾼으로 아래와 같은 역할을 한다.

  1. CPU 스케줄링과 프로세스 관리: CPU 소유권을 어떤 프로세스에 할당할지, 프로세스의 생성과 삭제, 자원 할당 및 반환을 관리
  2. 메모리 관리 : 한정된 메모리를 어떤 프로세스에 얼마큼 할당해야 하는지 관리
  3. 디스크 파일 관리 : 디스크 파일을 어떠한 방법으로 보관할지 관리
  4. I/O 디바이스 관리 : 마우스, 키보드 같은 I/O 디바이스들과 컴퓨터 간에 데이터를 주고받는 것을 관리

멀티 프로세스란?

사용자가 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 애플리케이션의 코드를 실행하는데 운영체제에서는 실행 중인 하나의 애플리케이션을 프로세스라고 부른다. 멀티 프로세스는 애플리케이션 단위의 멀티 태스킹을 의미한다.

 

멀티 프로세스들은 운영체제로부터 메모리를 할당 받아 서로 독립적으로 실행되기 때문에 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다. 또한 멀티 프로세스는 프로세스끼리 데이터를 주고받고 공유 데이터를 관리하는 IPC(Inter Process Communication)가 가능하다.

멀티 스레드란?

스레드는 운영체제로부터 메모리를 할당받아 실행되는 프로세스와 달리 프로세스로부터 자원을 할당받아 실행되는 작업의 흐름을 지칭한다. 멀티 프로세스가 애플리케이션 단위의 멀티 태스킹이라면 멀티 스레드는 애플리케이션 내부에서의 멀티 태스킹을 의미한다.

 

멀티 스레드는 하나의 프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에게 영향을 미칠 수 있다. 마지막으로 멀티 스레드는 스택 영역을 제외한 모든 영역을 한 프로세스 내에서 공유하기 때문에 IPC를 이용해 데이터를 공유하는 멀티 프로세스보다 빠르다.

멀티 프로세스와 멀티 스레드의 장단점

멀티 프로세스

장점

  • 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다.

단점

  • 독립된 메모리 영역이기 때문에 작업량이 많을수록(Context Switching이 자주 일어나서 주소 공간의 공유가 잦을 경우) 오버헤드가 발생하여 성능저하가 발생 할 수 있다.
  • Context Switching 과정에서 캐시 메모리 초기화 등 무거운 작업이 진행되고 시간이 소모되는 등 오버헤드가 발생한다.

Context Switching이란?

  • 한 프로세스에 할당된 시간이 끝나거나 인터럽트에 의해 발생하며 프로세스에 대한 메타데이터를 저장한 데이터인 PCB를 교환하는 과정을 말한다.
  • 동작 중인 프로세스가 대기를 하면서 해당 프로세스의 상태(Context)를 PCB(Process Control Block)에 보관하고, 대기하고 있던 다음 순서의 프로세스가 동작하면서 이전에 보관했던 프로세스의 상태를 복구하는 작업을 말한다.
  • CPU는 한 시점에 한 개의 프로그램만 실행하는데 컴퓨터가 많은 프로그램을 동시에 실행하는 것처럼 보이는 이유는 다른 프로세스와의 Context Switching이 매우 빠른 속도로 실행되기 때문이다.

멀티 스레드

장점

  • 멀티 프로세스에 비해 메모리 자원소모가 줄어든다.
  • 힙 영역을 통해서 스레드간 통신이 가능하기 때문에 프로세스간 통신보다 간단하다.
  • 스레드의 컨텍스트 스위칭은 프로세스의 컨텍스트 스위칭보다 비용이 더 적고 빠르다.

단점

  • 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에게 영향을 미칠 수 있다.
  • 데이터, 코드, 힙 영역을 공유하기 때문에 동기화 문제가 발생할 수 있다.
  • 동기화를 해결하기 위해 락을 과도하게 사용하면 성능이 저하될 수 있다.

작업 스레드 생성과 실행

멀티 스레드로 실행하는 애플리케이션을 개발할때 몇 개의 작업을 병렬로 실행할지 결정하고 메인 스레드를 제외한 추가적인 병렬 작업의 수만큼 스레드를 생성해주면 된다.

스레드 생성 방법

1. java.lang.Thread 클래스를 직접 객체화 해서 생성하는 방법

직접 생성하려면 아래와 같이 Runnable을 매개값으로 갖는 생성자를 호출해야 한다. Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어서 대입하여야 한다.

Thread thread = new Thread(Runnable target);

 

멀티 스레드 사용 예시로 비프음을 발생시키면서 프린트까지 하는 작업을 코딩해보겠다.

import java.awt.Toolkit;

public class BeepTask implements Runnable {
	@Override
	public void run() {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for (int i = 0; i < 5; i++) {
			toolkit.beep();
			try {
				Thread.sleep(500);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}
public class BeepPrintExample1 {

	public static void main(String[] args) {
		Runnable beepTask = new BeepTask();
		Thread thread = new Thread(beepTask);
		thread.start();
		
		for (int i = 0; i < 5; i++) {
			System.out.println("띵");
			try {
				Thread.sleep(500);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
}

 

다음과 같이 코딩한 다음 실행해 보면 비프음과 프린팅이 동시에 발생하는 것을 확인할 수 있다.

 

비교를 위한 코드

import java.awt.Toolkit;

public class BeepPrintExample2 {
	public static void main(String[] args) {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for (int i = 0; i < 5; i++) {
			toolkit.beep();
			try {
				Thread.sleep(500);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		for (int i = 0; i < 5; i++) {
			System.out.println("띵");
			try {
				Thread.sleep(500);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

위와 같이 Runnable 인터페이스를 구현한 객체를 생성하지 않고 스레드 생성없이 실행하면 비프음이 5번 들리고 나서 프린팅이 시작된다. 즉 비프음을 발생시키는 작업과 "띵"을 프린트 하는 작업이 동시에 처리가 안됨

 

2. Thread를 상속하는 하위 클래스를 만들어서 생성하는 방법

import java.awt.Toolkit;

public class BeepThread extends Thread {

	@Override
	public void run() {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for (int i = 0; i < 5; i++) {
			toolkit.beep();
			try {
				Thread.sleep(500);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
}
public class BeepPrintExample3 {
	
	public static void main(String[] args) {
		Thread thread = new BeepThread();
		thread.start();
		
		for (int i = 0; i < 5; i++) {
			System.out.println("띵");
			try {
				Thread.sleep(500);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

}

 

이런식으로 Runnable 인터페이스를 상속받은 구현 클래스가 아닌 Thread 클래스를 상속 받는 하위 클래스를 생성하여 메인 스레드에서 바로 Thread 생성 후 실행해도 같은 결과가 나온다.