Operating System Concepths, 10th edition을 학습하고 정리한 내용입니다.
학습 목표
- 쓰레드의 기본 구성 요소를 설명할 수 있고, 쓰레드와 프로세스의 차이를 설명할 수 있다.
- 멀티쓰레드 프로세스의 장점과 주요과제를 설명할 수 있다.
- Pthreads, Java 등 다양한 threading API를 사용해서 멀티쓰레드 어플리케이션을 설계할 수 있다.
- implicit threading의 다양한 접근법에 대해 설명할 수 있다.
- Windows와 Linux에서 쓰레드를 어떻게 다루는지 차이점을 설명할 수 있다.
Thread
쓰레드의 구성 요소
쓰레드는 CPU utilization의 기본 단위이다. 독립적으로 thread ID와 program counter와 register set과 stack을 가지고 있고, 같은 프로세스의 다른 쓰레드와 코드, 데이터 기타 운영체제 리소스를 공유한다.
멀티쓰레딩 예시
웹서버가 클라이언트의 요청을 처리하고 있는 상황이다. 웹서버가 싱글쓰레드로 동작하면 하나의 요청을 처리하는 동안 다른 클라이언트가 너무 긴 시간을 대기할 수도 있다.
요청이 올 때마다 새롭게 프로세스를 만들 수 있겠지만, 프로세스 생성이 시간, 리소스 측면에서 비효율적이다. 게다가 새로운 프로세스가 기존 프로세스와 같거나 비슷한 일을 한다면 굳이 오버헤드를 감수할 필요가 없다.
그래서 위와 같이 서버는 계속 요청을 받고 요청이 올때마다 새로운 쓰레드를 non-blocking으로 생성할 수 있다.
멀티쓰레딩 장점
- Responsirveness. 프로그램의 일부가 block돼도 사용자에 대한 응답성을 증가시킬 수 있다.
- Resource Sharing. 프로세스는 shared memory 혹은 message passing으로 리소스를 공유할 수 있어서 프로그래머가 별도로 작업해야하지만, 쓰레드는 기본적으로 메모리와 리소스를 공유한다.
- Economy. 프로세스 생성은 자원과 리소스를 할당하는데 비용이 많이 들지만, 쓰레드는 자원을 공유하므로 쓰레드 생성과 context switching이 프로세스보다 경제적이다.
- Scalability. 싱글 쓰레드 프로세스는 코어가 몇개든 하나의 작업만 수행할 수 있지만, 멀티 쓰레드 프로세스는 쓰레드가 여러 처리기에서 병렬적으로 수행될 수 있다.
멀티프로그래밍 주요과제
- Identifying tasks. 모든 작업이 병렬적으로 처리될 수 있는 것은 아니다. 예를 들어 합 연산은 멀티프로그래밍으로 나눠서 수행할 수 있지만 정렬은 순서가 중요하기 때문에 수행할 수 없다.
- Balance. 특정 코어에만 너무 많은 직업을 할당하면 안된다.
- Data splitting. 태스크가 나뉜것처럼, 태스크가 접근하고 조작하는 데이터 또한 개별 코어로 나눈다.
- Data dependency. 데이터가 두개 이상의 태스크에 종속적인지 확인하고, 그렇다면 태스크를 동기화시킨다.
- Testing and debugging : 병렬로 실행되므로 디버깅이 어렵다.
Amdahl's law
- 코어가 많다고 코어 개수에 비례해서 속도가 빨라지는 것은 아니다. serial하게 실행돼야 하는 태스크의 비율에 따라 다르다.
- speedup <= $\frac{1}{S + \frac{1-S}{N}}$
- S : serialy 하게 처리돼야하는 태스크의 비율 ( 병렬처리 할 수 없는 태스크의 비율)
- N : 코어의 개수
Thread Library
Pthreads
Pthread 예제 1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int sum;
void * runner(void *param);
int main(int argc, char *argv[])
{
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_create(&tid, &attr, 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);
}
컴파일 명령어 : gcc -pthread 4.11.c -o example
실행 명령어 : ./example 10
출력값 : 55
pthread_t tid : 새로 만들 쓰레드의 tid
pthread_attr_init() : 쓰레드의 설정 정보 설정. 위 코드에서는 기본 설정을 사용한다.
pthread_create() : 쓰레드 생성. tid, 설정정보, 실행할 함수, 그 외 파라미터를 입력한다. 위 코드에서는 runner()가 실행된다.
pthread_join() : 새로 만든 쓰레드가 합을 구할 때까지 기다린다.
Pthread 연습문제
지역변수가 쓰레드간 공유되고, 프로세스간 공유되지 않는 것을 보여주는 연습문제이다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wait.h>
#include <pthread.h>
int value = 0;
void * runner(void * param);
int main(int argc, char *argv[])
{
pid_t pid;
pthread_t tid;
pthread_attr_t attr;
pid = fork();
if(pid == 0) {
pthread_attr_init(&attr);
pthread_create(&tid, &attr, runner, NULL);
pthread_join(tid, NULL);
printf("CHILD: value = %d\n", value);
}
else if (pid > 0) {
wait(NULL);
printf("PARENT: value = %d\n", value);
}
}
void *runner(void *param) {
value = 5;
pthread_exit(0);
}
출력결과
CHILD: value = 5
PARENT: value = 0
부모 프로세스 $P_{0}$가 자식 프로세스$P_{1}$를 만들고 전역변수 value를 복사한다. 자식 프로세스에서 메인쓰레드 $t_{0}$와 자식쓰레드 $t_{1}$를 실행하는데, 자식쓰레드의 runner가 전역변수 value를 5로 바꾼다. 쓰레드끼리는 메모리를 공유하므로 자식 프로세스의 value는 5이다. 하지만 프로세스끼리는 공유하지 않기 때문에 부모 프로세스에서 value는 0이다.
Java Threads
Implicit Threading
Implicit Threading은 쓰레드의 생성과 관리를 프로그래머가 아닌 컴파일러나 런타임 라이브러리에 맡기는 것을 의미한다.
Thread Pools
웹서버가 요청이 들어올때마다 쓰레드를 생성하는 상황이다. 메모리와 CPU는 한계가 있기 때문에 쓰레드 생성을 제한해야한다. Thread Pools는 미리 여러 개의 쓰레드를 생성하고, 요청이 올때마다 쓰레드를 만드는 것이 아니라 미리 생성된 쓰레드를 사용한다. 그러다가 pool에 쓰레드가 더 없으면, 새로운 요청이 이전 요청이 끝날 때까지 대기한다.
Thread Pool의 장점
- 쓰레드를 생성하는 것보다 이미 존재하는 쓰레드를 사용하는 것이 더 빠르다.
- 쓰레드의 개수를 제한한다.
- 태스크의 실행 전략을 다양하게 사용할 수 있다. 일정 딜레이 이후에 실행할 수도 있고 주기적으로 실행할 수도 있다.
OpenMP
parallel하게 실행될 영역을 컴파일러에게 명시할 수 있다.
사용 예제
#include <stdio.h>
#include <omp.h>
int main(int argc, char *argv[])
{
#pragma omp parallel
{
printf("I am a parallel region.\n");
}
return 0;
}
시스템에 있는 코어 갯수만큼 쓰레드를 생성한다. #pragma omp parallel가 붙은 블락이 parallel하게 실행된다.
omp_set_num_threads(number); 를 추가해서 쓰레드 개수를 설정할 수 있다.
실행 시간 비교
#pragma omp parallel for를 사용하면 반복문에서 태스크를 멀티쓰레드로 나눠서 처리할 수 있다.
멀티쓰레드 사용
#include <stdio.h>
#include <omp.h>
#define SIZE 100000000
int a[SIZE], b[SIZE], c[SIZE];
int main(int argc, char *argv[])
{
int i;
for (i=0; i<SIZE; i++) {
a[i] = b[i] = i;
}
#pragma opm parallel for
for (i=0; i<SIZE;i++) {
c[i] = a[i] + b[i];
}
return 0;
}
컴파일 명령어 : gcc -fopenmp 4.20.3.c -o withParallel
시간 측정 명령어 : time ./withParallel
위 코드를 #pragma opm parallel for를 주석처리하고 실행한다.
컴파일 명령어 : gcc -fopenmp 4.20.3.c -o withoutParallel
시근 측정 명령어 : time ./withoutParallel
실행 결과
parallel하게 실행했을 때 시간이 단축된 것을 확인할 수 있다.
Operating-System Example
Windows Threads
Linux Threads
'CS > 운영체제' 카테고리의 다른 글
[OS] 인터럽트, 시스템 콜, 커널모드, 사용자모드 (0) | 2023.02.03 |
---|---|
[OS] 뮤텍스, 세마포어, 모니터 (0) | 2023.01.18 |
[OS] 동기화 문제, Peterson's solution, 하드웨어 명령어 (0) | 2023.01.15 |
[OS] CPU scheduling (0) | 2023.01.11 |
[OS] 프로세스 : 프로세스 개념, 컨텍스트 스위칭, 생성 및 통신 (0) | 2023.01.08 |