
공룡책(10판) Chapter 4 요약
여태까지의 프로세스는 싱글 스레드. 이제부터 조금씩 복잡해지겠지?
개요 _ Overview
스레드는 llightweight process로 CPU utilization의 기본 단위 cpu를 점유하는 기본 단위이다. 스레드 ID, 프로그램 카운터(PC), 레지스터 집합과 스택으로 구성된다.
전통적인 프로세스는 하나의 제어 스레드를 가지고 있으나, 다수의 제어 스레드를 가질 경우 프로세스는 동시에 여러 작업을 수행할 수 있다.
코드, 데이터, 파일 섹션은 공유하며 레지스터, 스택, 프로그램 카운터 정보는 스레드가 별도로 가진다.
- 멀티스레딩의 이점
- 응답성 (Responsiveness) : 응용 프로그램의 부분이 block 되어 있을 필요 없이 non-blocking으로 계속 실행된다. 즉 오래 걸리는 연산이 있어도 사용자에게 여전히 응답할 수 있다.
- 자원 공유 (Resource Sharing) : Shared-Memory나 Message-Passing은 프로세스 단위인데 비해 스레드간 리소스 전달은 코드, 데이터영역을 공유하기에 리소스 사용에 자유롭다. 위 그림 참고.
- 경제성 (Economy) : 프로세스 생성보다 스레드 생성이 경제적이고 컨텍스트 스위칭에 있어서도 스레드의 경우가 오버헤드가 적다.
- 규모 적응성 (Scalability) : 멀티프로세서 아키텍처에서 병렬 수행의 이점을 가진다.
다중 코어 프로그래밍 _ Multicore Programming
시스템 설계 추세는 단일 컴퓨팅 칩에 다수의 컴퓨팅 코어를 배치하는 것이다. 이러한 다중 코어에서 다중 스레드 프로그래밍은 이런 여러 컴퓨팅 코어를 효율적으로 사용하고 병행성을 향상시키는 기법을 제공한다.
- 병행성과 병렬성
- 병행성 (Concurrency) : 시분할과 같은 방법으로 동시에 실행하는 것처럼 보인다.
- 병렬성 (Parallelism) : 실제로 동시에 실행됨.
싱글코어에서는 상호 교대(interleaving) 방식으로 시분할 할때 끼워넣어 Concurrency 구현, 멀티코어에서는 시분할 + 병렬 처리로 구현
- 프로그래밍 도전과제
- 태스크 인식 (Identifying tasks) : task를 나눌 수 있는 영역을 찾는 것, task는 독립적이고 개별 코어에서 병렬 실행될 수 있어야 한다.
- 균형 (Balance) : 전체 작업에 균등한 기여도를 가지도록 task를 나누어야 한다.
- 데이터 분리 (Data splitting) : 개별 코어에 실행될 데이터를 나뉘어야 한다
- 데이터 종속성 (Data dependancy) : 둘 이상의 태스크가 실행될 때 데이터가 종속적인 경우 데이터의 동기화도 필요하다.
- 시험 및 디버깅 (Testing and debugging) : 다양한 실행 경로를 테스트, 디버깅해야 하기에 싱글스레드보다 어렵다.
- 병렬 실행 유형
- 데이터 병렬 실행 data parallelism : 동일한 데이터의 부분집합을 다수의 코어에 분배한 뒤 동일 연산을 실행하는데 초점
- 태스크 병렬 실행 task parallelism : 데이터가 아닌 테스크를 다수의 코어에 분배하여 각자 고유위 연산을 실행.
둘은 상호 배타적이지 않고 혼합해서 사용한다..만 distributed 분산 처리 (hadoop 등) 시스템 환경으로 넘어왔기에 크게 중요하지 않다.
- 암달의 법칙 (Amdahl’s Law)
순차 실행 구성요소와 병렬 실행 구성요소로 이루어진 app 에서 코어를 증가시켰을 때 성능 이득을 계산한다. 공식은 아래와 같다.
speedup <= 1/(S+((1-S)/N))
S = 시스템의 순차적으로 실행되는 요소 (serial) N = 코어의 개수
N이 무한대에 가까워진다고 하더라도 속도는 1/S 에 수렴하게 되어 코어를 아무리 달아도 성능 향상에는 한계가 있다고 하여 암달의 저주라고도 불린다.
다중 스레드 모델 _ Multithreading Models
스레드를 위한 지원에는
- 사용자 스레드(user threads) : 커널 서포트 없이 유저 모드 위에서 동작하는 스레드
- 커널 스레드(kernel threads) : OS가 직접 관리하는 스레드
가 있으며, 사용자와 커널 스레드는 연관 관계가 존재한다.
- 다대일 모델
많은 유저 스레드를 하나의 커널 스레드로 연결한다. 스레드 관리가 유저 스레드에 의해 행해지기에 효율적이나, 한 스레드가 봉쇄형 시스템 콜을 행할 경우 전체 프로세스가 봉쇄된다. 또한 다중 스레드가 다중 코어 시스템에서 병렬로 실행될 수 없다. 그래서 현재 이 모델은 거의 사용하고 있지 않다.
- 일대일 모델
각 사용자 스레드가 각각 하나의 커널 스레드로 연결된다. 병렬성을 제공하지만 사용자 스레드 생성에 각각 커널 스레드가 필요하기에 많은 커널 스레드가 시스템에 부담을 줄 수 있다.
- 다대다 모델
이 모델은 위 문제를 어느 정도 해결했다. 필요한 만큼 사용자 스레드를 생성하고, 상응하는 커널 스레드가 다중 처리기에서 병렬로 수행될 수 있다. 스레드가 block 시스템 콜을 발생하면 커널이 다른 스레드의 수행을 스케줄 할 수 있다. 이 모델의 경우에도 두 수준 모델(two -level model) 로 일대일 연결도 지원한다. 그러나 다대다 모델은 구현의 어려움, 코어 수의 증가로 스레드 제한의 중요성이 줄어들어 일대일 모델을 많이 사용한다.
스레드 라이브러리
프로그래머들에게 스레드를 생성하고 관리하기 위한 API를 제공한다. 사용자 수준 라이브러리, 커널 수준 라이브러리를 제공하는 방법이 있다. Pthread, Windows 및 Java 세 종류의 라이브러리라 주로 사용된다.
- POSIX Pthreads
- Windows thread
- Java thread : JVM, 운영체제에 종속적
- Pthreads
POSIX가 스레드 생성과 동기화를 위해 제정한 표준 API이다. 동작에 따른 정의를 명세할 뿐 구현은 아니다. Linux, MaxOS 등의 시스템에서 Pthreads 명세를 구현하고 있다. 아래는 하나의 스레드를 생성하는 예제이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int sum; /* this data is shared by the thread(s) */
void *runner(void *param); /* threads call this function */
int main(int argc, char *argv[])
{
pthread_t tid; /* the thread identifier */
pthread_attr_t attr; /* set of thread attributes */
/* 어트리뷰트 지정 */
pthread_attr_init(&attr);
/* 스레드 생성 */
pthread_create(&tid, &attr, runner, argv[1]); // 런너 설정, 명령어의 arg 입력
/* 스레드가 끝날때까지 대기 */
pthread_join(tid, NULL);
printf("sum = %d\n",sum);
}
/* The thread will execute in this function */
void *runner(void *param)
{
int i, upper = atoi(param);
sum = 0;
for (i = 1; i <= upper; i++)
sum += i;
pthread_exit(0); // 스레드 종료
}
해당 명세를 지정해 작성. 여기서는 main()
과 runner()
의 두 개 스레드를 가지게 되어 전역변수 sum을 공유한다.
- Windows 스레드 _ Windows Threads
Pthread 기법과 유사하며, Windows API를 제공한다. 여기서는 CreateThread()
함수에 의해 생성되고 WaitForSingleObject()
함수로 Pthread의 join과 같은 역할을 수행한다. 여러 스레드라면 WaitForMultipleObject()
를 사용한다.
- JAVA 스레드 _ Java Thread
JVM을 사용하는 어떠한 시스템에서도 사용할 수 있다.
- Thread 클래스를 상속 받은 클래스 생성, public void run() 메소드에 오버라이드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyThread1 extends Thread {
public void run() {
try {
while(true) {
System.out.println("thread is running...");
Thread.sleep(500);
}
} catch (InterruptedException ie) {
System.out.println("interrupted");
}
}
}
public class ThreadExample1 {
public static final void main(String[] args) {
MyThread1 thread = new MyThread1();
thread.start();
System.out.println("start");
}
}
- Runnable 인터페이스를 구현한 클래스 생성, public void run() 메소드에 오버라이드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyThread2 implements Runnable {
public void run() {
try {
while(true) {
System.out.println("thread is running...");
Thread.sleep(500);
}
} catch (InterruptedException ie) {
System.out.println("interrupted");
}
}
}
public class ThreadExample2 {
public static final void main(String[] args) {
Thread thread = new Thread(new MyThread2());
thread.start();
System.out.println("start");
}
}
- 람다식 사용 (JAVA 1.8 ~), 새로운 클래스 선언 없이 람다식으로 Runnable 인터페이스 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ThreadExample2 {
public static final void main(String[] args) {
Runnable task = () -> {
try {
while(true) {
System.out.println("thread is running...");
Thread.sleep(500);
}
} catch (InterruptedException ie) {
System.out.println("interrupted");
}
}
Thread thread = new Thread(task);
thread.start();
System.out.println("start");
}
}
- 부모 스레드의 대기 시에는 join() 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ThreadExample2 {
public static final void main(String[] args) {
Runnable task = () -> {
try {
while(true) {
System.out.println("thread is running...");
Thread.sleep(500);
}
} catch (InterruptedException ie) {
System.out.println("interrupted");
}
}
Thread thread = new Thread(task);
thread.start();
try {
thread.join();
} catch (InterruptedException ie) {
System.out.println("Parent thread is interrupted");
}
System.out.println("start");
}
}
암묵적 스레딩 _ Implicit Threading
다중 코어 처리의 지속적 성장에 따라 수천 개의 스레드를 가진 app이 등장하게 되었으며, 이런 상황에서 멀티스레딩을 구현하는 것은 어렵기에, 병렬, 병행 실행되는 app을 디자인할 때 스레드의 생성 및 관리를 개발자가 아닌 컴파일러와 라이브러리에 맡기는 전략을 암묵적 스레딩이라 한다.
이를 설계하기 위한 몇가지 접근법을 소개한다.
- 스레드 풀
여러 개의 스레드를 풀(pool)에 넣고 꺼내서 쓰겠다는 것으로, 프로세스를 시작할 때 일정 수의 스레드를 미리 풀로 만들어둔다. 평소에는 일감을 기다리다가 요청이 들어 왔을 때 풀의 한 스레드에 작업을 할당한다. 풀에 스레드가 없다면 가용 스레드가 나올 때까지 기다린다.
이 기법은 기존 스레드를 할당하기에 새 스레드를 만드는 것보다 빠르고, 스레드 개수에 제한을 두기에 많은 수를 병렬 처리할 수 없는 시스템에 도움이 된다. 또한 태스트 생성 방법을 태스크에서 분리하는 작업(ex. 스케줄링 등) 을 할 수 있는 등의 장점이 있다.
- Fork & Join
명시적 스레딩이지만 암시적 스레딩에도 쓰일 수 있다. 스레드가 직접 fork 되지 않고, 라이브러리에서 스레드 수를 관리한다.
- OpenMP
C, C++, FORTRAN으로 작성된 API와 컴파일러 지시어로, 이 컴파일러 지시어를 주어서 병렬 처리를 할수 있다. 개발자가 병렬적 처리를 하기 위한 병렬 영역(parallel regions)을 지정한다.
#pragma omp parallel
가 지시된 영역은 병렬로 돌려달라는 것으로, 시스템 코어 개수만큼 스레드 생성한다.
omp_set_num_threads(4); 같은 명령어로 스레드 수를 제한할 수도 있고 #pragma omp parallel for 로 반복문을 병렬화 할 수 있다. 여담으로 반복문 중 for만 가능하다고 한다.
- 기타
- Grand Central Dispatch (GCD) : 맥이나 IOS에서 사용. 개발자가 병렬로 실행될 코드 섹션을 식별할 런타임 라이브러리, API 및 언어 확장의 조합이다. 태스크를 디스패치 큐에 넣어 스케줄 한다.
- Intel 스레드 빌딩 블록 (Intel TBB, Threading Building Block) : C++ 에서 병렬 응용 프로그램 설계를 지언하는 템플릿 라이브러리이다.
다음은 CPU 스케줄링~