CS - Process vs Thread Deep
Process 와 Thread
-
Process 는 OS에서 관리하는 실행되는 프로그램의 단위이다.
-
Process 안에는 최소 1개의 흐름이 있는데, 그 흐름을 Thread 라고 하며, 여러 Thread 가 존재할 수도 있다.
-
우리가 CPU로 부터 연산을 한다고 하면, Thread 단위로 연산을 진행한다.
-
컴퓨터의 세계는 3개의 레이어로 구성되어있다.
-
User Mode
-
프로세스가 동작한다.
-
시스템 콜과 인터럽트를 통해 커널 OS로 부터 받은 가상 메모리라는 제한된 공간을 받는다.
-
메모리는 무조건 가상의 공간을 부여받는다.
-
그렇게 하는 이유는 실제 Ram에 지금 공간이 부족할 수 있으므로, 하드디스크에서 용량을 가져와서 가상 메모리의 일부로 부여할 수도 있기 때문이다.
-
동작 못하는 것보다는 일시적으로라도 그렇게 사용하는게 낫기 때문이다.
-
-
스레드들은 Process의 가상 메모리라는 제한된 공간 속에서만 동작한다.
-
그 가상 메모리 안에 각 스레드별로 본인만 사용할 수 있는 Thread Local Storage & Stack 이 있고, 모든 스레드가 공유하는 공간인 Heap이 있다.
-
그러다보니 스레드가 여러개면 동시성 이슈가 발생하고, 동기화가 필요해진다.
-
-
Kernel Mode
-
User Mode 에서 프로세스가 동작하는데에 필요한 가상 메모리(Virtual Memory)를 할당해준다.
-
필요한 작업을 수행한 후, 커널은 다시 사용자 모드로 전환한다.
-
-
H/W
- 1차 저장소인 메모리와, 2차 저장소인 디스크가 있다.
-
Thread 간의 데이터 공유 방법
-
동시성, 동기화에 대한 이슈를 해결하는 그런 공유가 아닌, 진짜 A 스레드에서 B 스레드로 데이터를 전달하는 것을 말한다.
-
이 때는 Queue 를 활용하거나, static 변수를 활용하거나 할 수 있다.
-
현업에는 Kafka 라는 곳에 데이터를 넣고, 컨슈머가 가져가는 방법을 많이 사용한다.
-
다음은 Queue 를 활용한 데이터 공유 방법이다.
public class ThreadTest { private static final int MAX_ITEMS = 500; private static final Queue<Integer> sharedQueue = new LinkedList<>(); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); executorService.submit(() -> { for (int i = 1; i <= MAX_ITEMS; i++) { sharedQueue.add(i); System.out.println("Pushed: " + i); // Simulate some work being done try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }); executorService.submit(() -> { for (int i = 0; i < MAX_ITEMS; i++) { if (!sharedQueue.isEmpty()) { int item = sharedQueue.poll(); System.out.println("Popped: " + item); // Simulate some work being done try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }); executorService.shutdown(); } }
-
결과는 다음과 같다. ( 뒤죽박죽으로 나온다. )
Popped: 1 Pushed: 1 Pushed: 2 Popped: 2 Pushed: 3 Popped: 3 Pushed: 4 Popped: 4 Pushed: 5 Pushed: 6 Pushed: 7 Pushed: 8 . . . Pushed: 495 Pushed: 496 Pushed: 497 Pushed: 498 Pushed: 499 Pushed: 500
-
이렇게 할 경우, 스레드에 값을 제대로 전달하지 못한다.
-
동기화가 이루어지지 않기 때문이다.
-
하나의 Queue 에 대해서, 읽는 것과 쓰는 것이 동시에 접근하면 문제가 발생한다.
-
이는 Lock 으로 해결할 수 있다.
public class ThreadTest { private static final int MAX_ITEMS = 500; private static final Queue<Integer> sharedQueue = new LinkedList<>(); private static final Object lock = new Object(); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); executorService.submit(() -> { for (int i = 1; i <= MAX_ITEMS; i++) { synchronized (lock) { sharedQueue.add(i); System.out.println("Pushed: " + i); } // Simulate some work being done try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }); executorService.submit(() -> { for (int i = 0; i < MAX_ITEMS; i++) { synchronized (lock) { if (!sharedQueue.isEmpty()) { int item = sharedQueue.poll(); System.out.println("Popped: " + item); } } // Simulate some work being done try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }); executorService.shutdown(); } }
-
혹은 아래처럼 동기화를 보장해주는 BigQueue 와 LinkedBlockingQueue 를 활용할 수도 있다.
public class ThreadTest { private static final int MAX_ITEMS = 500; private static final BlockingQueue<Integer> sharedQueue = new LinkedBlockingQueue<>(); public static void main(String[] args) { Thread pushThread = new Thread(() -> { for (int i = 1; i <= MAX_ITEMS; i++) { try { sharedQueue.put(i); System.out.println("Pushed: " + i); // Simulate some work being done Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread popThread = new Thread(() -> { for (int i = 0; i < MAX_ITEMS; i++) { try { int item = sharedQueue.take(); System.out.println("Popped: " + item); // Simulate some work being done Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } }); pushThread.start(); popThread.start(); try { pushThread.join(); popThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
결과는 다음과 같다.
Pushed: 1 Popped: 1 Pushed: 2 Popped: 2 Pushed: 3 Popped: 3 Pushed: 4 Popped: 4 Popped: 5 Pushed: 5 Pushed: 6 Popped: 6 Pushed: 7 Popped: 7 . . . Pushed: 499 Popped: 499 Pushed: 500 Popped: 500
Process 간의 데이터 공유 방법
-
File 에 저장하여 읽고 쓰게 할 수 있음.
-
Kafka 와 같은 메시지 버스를 활용 할 수 있음.
-
웹 소켓으로도 가능.