Amdahl's Law
암달의 법칙은 컴퓨터 시스템의 일부를 개선할 때, 전체적으로 얼마만큼의 최대 성능 향상이 있는지 계산하는데 사용된다.
병렬 컴퓨팅을 할 경우, 일부 병렬화가 가능한 작업들은 사실상 계산에 참여하는 컴퓨터의 개수에 비례해서 속도가 늘어난다. 하지만 암달의 법칙에 의해 아무리 컴퓨터의 개수가 늘어나더라도 속도의 한계는 정해져있다.
병렬 vs 병행
- 병행(Concurrent)은 멀티스레드 프로그래밍을 의미한다.
- 병렬(Parallel)은 멀티코어 프로그래밍을 의미한다.
- 따라서 여기서는 병행 프로그래밍(동시성 프로그래밍, 멀티스레드 프로그래밍)을 알아본다.
Context Switching
그렇다면 어떻게 동시에 프로그램들이 실행될 수 있는 것일까?
컴퓨터는 0초부터 1초까지 시간을 잘게 쪼갠다.(Time Silcing) 그리고 프로그램이 여러개가 있으면, 잘게 쪼개진 각 시간만큼 특정 시간에 프로세스를 실행시켜준다. 이때 컴퓨터가 속도가 빠르기 때문에 프로세스1,2,3,4가 마치 동시에 실행되고 있는 것처럼 보이는 것이다. 즉, 프로세스 1은 실행됐다가, 안됐다가를 반복하는 것이다. 그렇다면 프로세스1이 실행되고, 나중에 다시 실행되기 위해서는 어디까지 실행이 됐었는지, 메모리는 어떤 부분을 사용하고 있었고를 기억해야한다. 이런 부분들을 Context Switching이 발생한다고 하고, 이 부분에서 시간이 많이 소요된다. (Overhead가 발생한다.)
정리하면, 동작중인 프로세스가 바뀔 때, 프로세스는 현재 자신의 상태(context 정보)를 일단 보존한 후, 새롭게 동작을 개시하는 프로세스는 이전에 보존해 두었던 자신의 context 정보를 다시 복구한다. 이와 같은 현상 Context Switching이라고 한다.
이때, Thread의 context 정보는 프로세스보다 적기때문에 Thread의 Context Switching은 가볍게 행해지는 것이 보통이다.
하지만, 실제로 Thread와 프로세스의 관계는 JVM 구현에 크게 의존한다. 그래서 플랫폼이 같아도 JVM의 구현방법에 따라 프로세스와 Thread의 관계는 달라질 수 있다.
Process
각각의 프로세는 메모리 공간에서 독립적으로 존재한다. 또한 각 프로세스는 자신만의 메모리 구조를 가진다.
프로세스 1,2,3가 있을때, 크기 자체는 다를 수 있지만 모두 같은 구조의 메모리 공간을 가진다. 그리고 각각 독립적인 만큼 다른 프로세스의 메모리 공간에 접근할 수 없다.
IPC
프로세스 1에서 2로 직접 접근할 수 없다. 따라서 프로세스 간의 통신을 위해 프로세스 간의 통신을 하는 특별한 방식이 필요하다. 즉, 프로세스 간의 통신하는 방법은 IPC이다.
Thread
프로세스가 여럿이 병렬적으로 실행되기 위해서는 필연적으로 Context Switching이 발생할 수 밖에 없다. 이 것을 해결할 수 있는 것이 Thread이다.
하나의 프로세스 내에 존재하는 여러 개의 실행 흐름을 위한 모델이다.
Thread도 시간을 쪼개서 사용을 반복한다. 즉, Thread도 Context Switching이 발생한다.
메모리 공간에서의 Thread
하나의 프로세스가 가지는 메모리를 여러 개의 Thread가 공유한다는 뜻이다. 이때, process 간의 전환 비용보다 Thread 간의 전환 비용이 더 적다.
Thread vs Process
Thread는 프로세스 안에 존재하는 실행흐름이다.
Thread는 프로세스의 heap, static, code 영역 등을 공유한다. 즉, stack 영역을 제외한 메모리 영역을 공유한다.
따라서 Thread가 code 영역을 공유하기 때문에 프로세스 내부의 Thread들은 프로세스가 가지고있는 함수를 자연스럽게 모두 호출할 수 있다. 즉, Thread는 IPC 없이도 Thread 간의 통신이 가능하다.
결론적으로 A, B Thread는 통신하기 위해 heap 영역에 메모리 공간을 할당하고, 두 Thread는 자유롭게 접근할 수 있으므로, Multi-Process 환경보다 Multi-Thread 환경이 프로그래밍이 쉽다.
물론 Thread도 프로세스처럼 스케쥴링의 대상이다. 따라서 Context Switching이 발생한다. 하지만 앞서 말했듯이 공유하고 있는 메모리 영역 덕분에 Context Switching으로 발생하는 Overhead가 프로세스에 비해 작다.
반면에 Thread는 공유 메모리 영역이 존재하므로, Thread 간에 자원을 획득하기 위한 경쟁이 일어난다. 따라서 메모리가 많고, 컴퓨팅 속도가 빠를때는 오히려 Multi-process 환경이 더 유리한 경우도 있을 수 있다.
Multi-Thread 실행방식
동시에 2가지 흐름이 흐르게 할 수 있다. 즉, 동시에 여러 작업을 할 수 있다.
Thread를 만드는 방법 1
먼저, Thread 클래스를 상속받아서 작성하는 방법이 있다. 이때, Thread의 run() 메서드를 상속받는 클래스에서 반드시 Overriding 해줘야한다.
여기서 주의할 점은 run 메서드를 오버라이딩 하지만, 실제 호출하는 메서드는 start() 여야한다.
Thread 만드는 규칙
- Thread 클래스를 상속받는다.
- run 메서드를 Overriding한다. 여기에 동시에 실행시키고 싶은 코드를 작성한다.
- Thread는 start() 메서드로 시작한다.
다음은 Thread를 상속받는 클래스이다.
public class MyThread extends Thread {
private String str;
public MyThread(String str) {
this.str = str;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.print(str);
try {
Thread.sleep(1000); // 1초간 쉰다.
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
그리고 위 클래스를 이용해 Main Thread에 두가지 흐름을 추가했다. main()메서드가 실행되고, mt1.start()메서드가 실행된다. 이때, start() 메서드는 Thread가 실행되기위한 준비작업을 해주고, 자기자신이 가진 run() 메서드를 호출시켜준다.
즉, main 메서드가 실행되면서 main Thread가 실행이 된다. 그렇게 한줄씩 실행하다가 start()를 만나면 이게 Thread준비를 하고, 자신의 run()메서드를 실행하면서 하나의 Thread가 실행된다. 그리고 main Thread는 다음 줄을 실행해나간다.
이렇게 Main Thread, mt1, mt2 Thread 총 3가지 흐름이 된다. 이때, main thread만 이름이 main이고, 직접 실행한 Thread는 이름이 Thread-0, Thread-1처럼 나온다.
package theory.thread;
public class MyThreadExam {
public static void main(String[] args) {
String name = Thread.currentThread().getName();
System.out.println("thread name = " + name);
System.out.println("start!");
// 1초마다 * 를 10번 출력하는 프로그램
MyThread mt1 = new MyThread("*");
// 1초마다 + 를 10번 출력하는 프로그램
MyThread mt2 = new MyThread("+");
mt1.start();
mt2.start();
System.out.println("end!");
}
}
결론적으로 Thread를 배우기 전에는 프로그램은 main 메서드가 종료되면 끝나는데, 그 이후로는 모든 Thread가 종료되어야 프로그램이 종료된다고 생각해야 한다.
Thread 만드는 방법 2
두번째 Thread 만드는 방법으로 Runnable Interface를 구현하여 만드는 방법이 있다. 하지만 Runnable에는 start 메서드가 없다. 따라서 Xxx 클래스가 Runnable을 구현하도록 만들면 Thread가 이 클래스를 가지도록 해야한다.
즉, 정리하면
- Runnable Interface를 구현한다.
- run() 메서드를 오버라이딩한다.
- Thread 인스턴스를 생성하는데, 생성자에 Runnable 인스턴스를 넣어준다.
- Thread가 가지는 start 메서드를 호출해준다.
public class MyRunnable implements Runnable{
private String str;
public MyRunnable(String str) {
this.str = str;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println("Thread Name : " + name);
for (int i = 0; i < 10; i++) {
System.out.print(str);
try {
Thread.sleep(1000); // 1초간 쉰다.
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class MyThreadExam2 {
public static void main(String[] args) {
String name = Thread.currentThread().getName();
System.out.println("thread name = " + name);
System.out.println("start!");
MyRunnable mr1 = new MyRunnable("*");
MyRunnable mr2 = new MyRunnable("+");
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr2);
t1.start();
t2.start();
System.out.println("end!");
}
}
Thread와 공유 객체
공유 객체란 하나의 인스턴스를 여러 개의 Thread가 함께 사용하는 경우를 말한다. 즉, 인스턴스는 하나인데 사용하려는 Thread는 여러 개인 경우이다.
이때, 자바에서는 synchronized 키워드를 이용해 공유 객체 동시에 실행되면 안되는 메서드나 블록을 보호할 수 있다. 여기서 한 스레드가 진행중인 작업을 다른 스레드가 간섭하지 못하도록 막는 것을 '스레드의 동기화'라고 한다.
다음과 같은 코드가 있을 때,
public class MagicBox {
public void star() {
String name = Thread.currentThread().getName();
System.out.println("Thread Name : " + name);
for (int i = 0; i < 5; i++) {
System.out.print("*");
try {
Thread.sleep(1000); // 1초간 쉰다.
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Player extends Thread {
private String name;
private MagicBox magicBox;
public Player(String name, MagicBox magicBox) {
this.name = name;
this.magicBox = magicBox;
}
@Override
public void run() {
if ("kim".equals(name)) {
magicBox.star();
} else {
System.out.println("nothing");
}
}
}
public class MagicBoxExam {
public static void main(String[] args) {
MagicBox magicBox = new MagicBox();
Player p1 = new Player("kim", magicBox);
Player p2 = new Player("kim", magicBox);
p1.start();
p2.start();
}
}
MagicBox의 star 메서드에 스레드 동기화 관련 키워드가 없으면 다음과 같이 공유 객체 메서드에 마음대로 접근하여 뒤죽박죽 출력된다.
Thread Name : Thread-0
*Thread Name : Thread-1
*********
하지만 star 메서드에 synchornize 키워드를 붙이면 다음과 같이 공유 객체 메서드에 동시 접근을 보호해 순차적으로 출력된다.
Thread Name : Thread-0
*****Thread Name : Thread-1
*****
인용
https://elgaabeb.wordpress.com/
https://velog.io/@gparkkii/ProgramProcessThread
https://youtu.be/tF9C3rG7Xtw?si=MeXgOnp8CXM8qje2
https://velog.io/@hoha/JAVA-Thread%EC%99%80-%EA%B3%B5%EC%9C%A0%EA%B0%9D%EC%B2%B4
'Language > Java' 카테고리의 다른 글
동시성 문제와 ThreadLocal (0) | 2023.09.21 |
---|---|
싱글쓰레드와 멀티쓰레드, 싱글코어와 멀티코어 (0) | 2023.09.21 |
I/O - 다양한 IO객체들, 객체 직렬화 (0) | 2023.09.18 |
I/O - Decorator Pattern (0) | 2023.09.18 |
Java I/O - IO Stream (0) | 2023.09.18 |