1. Overview
🍠 Thread?
CPU 이용의 기본 단위
= Lightweight process
Thread의 구성 요소
🛷 각 쓰레드별 요소
- Thread ID
- 프로그램 카운터(PC)
- 레지스터 집합
- 스택
📝 같은 프로세스의 다른 쓰레드와 공유 요소
- 코드
- 데이터
- 열린 파일 / 신호 등의 OS 자원
🏐 모식도
🥎 Muti-thread programming 의 장점
- 응답성(Responsiveness)
- 다중 쓰레드화 하면 어느 한 요청의 응답시간이 길어지더라도 그 외 쓰레드로 다른 요청을 처리할 수 있다.
- 자원 공유(Resource Sharing)
- 기존의 프로세스간 통신(IPC)에서는 Shared Memory 혹은 Message Queue 방식을 사용해 자원을 공유했다.
- 반면 쓰레드 끼리는 그들이 속한 프로세스 내에서 코드, 데이터 등을 별도의 작업 없이 공유할 수 있다.
- 경제성(Economy)
- 프로세스의 생성이나 프로세스간의 Context switching은 쓰레드의 그것에 비해 오버헤드가 크다.
- 규모 적응성(Scalability)
- Multi-core 환경에서 각각의 쓰레드가 각기 다른 core에서 Parallel하게 실행될 수 있다.
2. Multicore Programming
🏀 병행성(Concurrency)과 병렬성(Parallelism)
병행성(Concurrency)
- 여러 작업을 시분할하여 실행함으로써 동시에 실행되는 것 처럼 실행(논리적 개념)
- 기존 Single-core 시스템에서의 병행성
- T5가 추가된다면 다른 쓰레드 조각 사이 사이에 끼워져 실행된다.
병렬성(Parallelism)
- 여러 작업을 실제로 여러 작업 처리기(CPU)에서 동시에 병렬로 실행(물리적 개념)
- Multi-core 시스템에서의 병렬성
- 병렬성(Parallelism) 없이 병행성(Concurrency)를 가질 수 있다.
🎈 Multi-core 프로그래밍 고려사항
- 태스크 인식(Identifying tasks)
- 수행해야 할 전체 작업을 분석하여 독립된 병행 가능 task로 나눌 수 있어야 한다.
- 이상적인 task는 서로 독립적이고 개별 코어에서 병렬 실행이 가능하다.
- 균형(balance)
- 각 task들이 균등한 기여도를 갖도록 나눌 수 있어야 한다.
- 데이터 분리(Data splitting)
- task를 나누는 것처럼 task가 접근하는 data 또한 개별 코어에서 가용하도록 분리할 수 있어야 한다.
- 데이터 종속성(Data dependency)
- 둘 이상의 task가 접근하는 data에 대해서는 task의 수행을 동기화 해야한다.
- 테스트 및 디버깅(Test and debugging)
🔑 Amdahl's Law
- Core가 많을수록 무조건 좋은가?에 대한 답을 얻을 수 있음
- 기본 공식
- S: 작업 중 반드시 순차적으로 실행되어야 하는 구성요소의 비율(병렬 X)
- N: 코어의 개수
상황별 예시
- S = 0.25, N = 2 ⇒ speedUp = 1.6
- 단순히 이론적으로만 생각하면 1개의 코어로 10초가 걸리면 2개의 코어가 있다면 속도는 2배가 되어야 한다.
- 하지만 전체 작업 중 25% 정도만 병렬이 아닌 순차적으로 실행되어야 한다면 총 처리 속도는 2배가 아닌 1.6배가 더 빨라질 것이다.
- S = 0.25, N = 4 ⇒ speedUp = 2.28
- 위의 예시와 비교해서 코어의 수가 2배가 되어 단순 계산으로 속도는 3.2배가 되어야 한다.
- 하지만 전체 작업 중 25%가 순차적 실행이므로 Amdahl의 계산에 의하면 2.28배 빨라질 뿐이다.
- N과 S에 따른 속도 향상 그래프
- S = 0.25, N = 2 ⇒ speedUp = 1.6
- 위 예시로 미루어 보아 순차 실행 요소가 포함되어 있다면 단순 core 수의 확장이 무조건적인 성능의 향상을 보장하지는 않는다.
🔦 병렬(Parallelism) 실행의 유형
Task 병렬 실행
- task(쓰레드)를 다수의 core에 분배
- task는 각자 고유한 연산을 수행
Data 병렬 실행
- 동일한 Data의 부분 집합을 다수의 core에 분배
- 각 core는 동일한 연산 수행
3. Multithreading Models
🔑 커널(kernel) 스레드와 사용자(user) 스레드
Kernel thread
- OS에 의해 직접 지원, 관리됨
- 현대 모든 운영체제에서 지원
User thread
- 커널의 윗단에서 지원
- 커널의 직접적 지원 X
Kernel, User thread
☝ 다대일 모델 (Many-to-One Model)
다수의 user thread를 하나의 kernel thread에 할당
쓰레드 관리는 user space의 쓰레드 라이브러리가 담당하므로 효율적
단점: 하나의 user thread가 blocking system call을 하면 나머지 쓰레드들이 전부 기다려야함
모식도
✌ 일대일 모델 (One-to-One Model)
user thread와 kernel thread를 하나씩 매핑
하나의 user thread가 blocking system call을 해도 다른 쓰레드가 기다리지 않음
단점: 하나의 user thread를 만들려면 kernel thread도 생성해야 하며 시스템에 오버헤드로 작용
모식도
👌 다대다 모델 (Many-to-Many Model)
여러 user thread와 kernel thread를 매핑
필요한 만큼의 user thread를 생성이 가능하고 그에 상응하는 kernel thread가 병렬 실행될 수 있다.
blocking system call에 유연하다
최근 시스템 코어 수가 증가함에 따라 일대일 모델을 많이 사용하고 일대일 모델과 다대다를 혼용한 Two-level Model도 많이 사용한다.
다대다 모델 모식도
4. Thread Library
- 개발자에게 쓰레드를 생성하고 관리하기 편한 API를 제공
- 주로 사용되는 Thread Library
- POSIX Pthreads
- Windows thread library
- Java thread libaray
🥎 Pthreads
POSIX(IEEE 1003.1c)에서 쓰레드 생성, 동기화를 위해 제정한 표준 API
쓰레드의 동작에 대한 명세일 뿐, 구현은 아니다.
Pthread 예제
#include <pthread.h> int sum; void *runner(void *param); int main(int argc, char *argv[]) { pthread_t tid; pthread_create(&tid, NULL, runner, argv[1]); pthread_join(tid, NULL); printf("sum = %d\n", sum); } void *runner(void *param) { int i, upper = atoi(param); sum = 0; for (i = 0; i <= upper; i++) { sum += i; } pthread_exit(0); }
- pthread_create: Java의 new Thread()와 같이 새로운 쓰레드를 생성
- pthread_join: 자식 쓰레드의 동작이 끝날때 까지 대기
- void *runner: Java의 Runnable.run() 과 같이 쓰레드의 동작을 정의하는 함수
- 쓰레드끼리는 전역 변수를 공유하므로 main 함수에서 sum을 프린트하면 쓰레드에서 sum을 수정한 값 그대로 출력된다.
- ex) ./a.out 10 ⇒ "sum = 55" 출력
⚾ Java Thread
🏓 Thread를 생성하는 방법
자바에서 쓰레드를 작성하는 방법은 Thread 클래스를 상속 받는 것, Runnable 인터페이스를 구현하는 것 2가지 방법이 있다.
아래는 Thread, Runnable 두 가지 방법에 대해 동일한 예제를 적용하여 어떤 차이가 있는지 기록한다.
공통예제: 1초~6초 사이의 딜레이를 주고 각 쓰레드의 시작, 끝 시간에 print문으로 동작을 확인
Thread
- 내부에 여러 메소드가 존재하며 이 또한 Runnable 인터페이스의 구현체이다.
여러 메소드 중 run() 메소드만 오버라이드하면 쓰레드를 작성할 수 있다.
예제 코드
Public class MyThread extends Thread { private static final Random random = new Random(); @Override public void run() { String threadName = Thread.currentThread().getName(); System.out.println("- " + threadName + " has been started"); int delay = 1000 + random.nextInt(4000); try { Thread.sleep(delay); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("- " + threadName + " has been ended (" + delay + "ms)"); } }
Runnable
- Thread와 달리 메소드가 run() 하나밖에 없음
- 인터페이스이므로 implements 하는 것 외에는 Thread와 동일
- 예제 코드(run 함수 내부는 Thread와 동일)
public class MyRunnable implements Runnable { private static final Random random = new Random(); @Override public void run() { ... } }
예제 실행
- Thread의 실행은 해당 인스턴스의 start()를 호출(run이 아님)
- Runnable은 Runnable형 인자를 받는 생성자를 통해 별도 Thread 객체 생성 후 start() 메소드 호출
- 예제 코드
public class ThreadRunner { public static void main(String[] args) { // Thread object 생성 Thread thread1 = new MyThread("Thread #1"); Thread thread2 = new MyThread("Thread #2"); // Runnable object 생성 Runnable runnable1 = new MyRunnable(); Runnable runnable2 = new MyRunnable(); Thread thread3 = new Thread(runnable1, "Thread #3"); Thread thread4 = new Thread(runnable2, "Thread #4"); // start all threads thread1.start(); thread2.start(); thread3.start(); thread4.start(); } }
- 실행 결과
Thread와 Runnable의 차이
- Thread 상속 시 다중 상속 불가능
- Runnable은 인터페이스 이므로 다중 상속 가능, 실제로 Runnable을 많이 사용
🍠 Thread를 실행하는 또 다른 방법
강의에서 언급했듯이 Java에서 쓰레드는 인스턴스 생성후 start() 메소드를 호출해서 실행할 수 있다.
Java 1.5 부터는 Executor 인터페이스를 제공하여 새로운 방식으로 쓰레드를 실행할 수 있게 되었다.
Executor.execute()로 쓰레드를 실행하는 방법
Executor service = new Executor(); service.execute(new Task());
- execute의 인자는 Runnable을 넘겨주어야 한다.
- Runnable은 리턴값을 갖지 않는다!
Executor.submit()으로 쓰레드를 실행하는 방법
Class SumTo5 implements Callable<Integer> { public Integer call() { int sum = 0; for (int i = 1; i <= 5; i ++) { sum += i; } return new Integer(sum); } } public class Test { public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); Future<Integer> result = threadPool.submit(new SumTo5()); } try { System.out.println("sum = " + result.get()); } catch (InterruptedException | ExecutionException ie) { ... } }
- submit의 인자로 Callable을 넘겨준다.
- Callable은 쓰레드의 실행결과를 Future 객체로 반환한다.
- 메인 쓰레드에서 Future 객체에서 get()을 하면 쓰레드의 결과값을 기다렸다가 가져와서 사용할 수 있다.
5. Implicit Threading(암묵적 쓰레딩)
- 쓰레드의 생성과 관리 책임을 개발자 → 컴파일러나 런타임 라이브러리에 넘겨주는 것
🎈 Thread Pool
- 기존 쓰레드 생성 방식의 문제점
- 새로운 쓰레드가 필요할때 마다 new 키워드로 매번 새로운 쓰레드 인스턴스를 생성하는 것은 오버헤드가 크다.
- 만약 쓰레드가 잠깐의 작업을하고 바로 폐기되어야 하고 한번에 여러 개의 쓰레드를 필요로 한다면 오버헤드는 더욱 커진다.
- 프로세스를 시작할 때, 일정한 수의 쓰레드를 만들어 pool 안에 넣어 두었다가 필요할 때마다 꺼내서 쓰는 방식
🏐 Fork Join
🔦 OpenMP
'OS' 카테고리의 다른 글
5. CPU Scheduling (0) | 2021.05.31 |
---|
댓글