스레드: 실행흐름(thread of execution)을 줄려서 스레드라 부름.
프로세스 1개당 1개의 스레드 프로세스 1개당 여러개의 스레드
프로세스와 스레드의 차이
프로세스:
프로세스는 프로세스가 새로운 프로세스를 생성하면 각 각 독립적인 메모리 영역을 소유한다. 이는 코드, 전역변수, 지역변수 모두에 해당한다.
스레드:
새로운 스레드를 생성 할 경우 부모와 자식은 stack(지역변수)만 다르고 독립적인 데이터 영역을 소유한다.
하지만 이외에 전역변수와, 코드는 같은 메모리 영역을 공유한다.
프로세스와 스레드 관련 함수.
프로세스:
- fork()
- exit()
- waitpid()
스레드:
- pthread_create()
- pthread_exit()
- pthread_join()
- pthread_ yield()
- pthread_attr_init()
- pthread_attr_destroy()
pthread라고 하는 이유는 posix이란 라이브러리에 들어있는 스레드 관련 함수.
프로세스와 스레드의 비교
프로세스
프로세스는 전역변수(global)를 공유하지 않는다.
함수 X는 i를 매개변수로 입력받아 전역변수에 i을 대입한다.
부모 프로세스는 전역변수에 1을 저장하고 자식프로세스의 종료를 기다린다.
자식 프로세스는 전역변수에 2를 저장하고 종료한다.
출력결과: 1
자식 프로세스는 print를 하지 않고 exit하기 때문이다. exit은 프로그램 종료코드이다.
스레드
스레드는 전역변수(global)를 공유한다. 하지만 지역변수를 공유하진 않는다.
pthread_create로 새로운 스레드를 생성한다. 이 때 매개변수로 전달된 함수 X는 새로이 만들어진 스레드에서 실행된다.
부모 스레드는 global = 2를 실행하고 자식 스레드는 함수 X의 global = 1을 실행하고 종료한다. 부모는 global에 2를 저장하고 자식 스레드는 1을 저장하는데 둘 다 같은 메모리 영역에 저장한다는 것이 프로세스와 다른점이다. 따라서 실행 결과는 메모리에 조금이라도 늦게 접근한 스레드의 결과가 된다. 2가 출력 될 수도, 1이 출력 될 수도 있다.
출력 결과: 1 또는 2 둘 중 하나가 출력된다.
한 프로세스 안의 모든 스레드들이 공유하는 항목
한 프로세스 안의 모든 스레드들이 공유하는 항목은 즉 운영체제가 프로세스를 관리하는 요소이다.
각 스레드마다 독립적인 항목
운영체제가 스레드를 관리하는 요소들이다.
스레드 모델
한 개의 프로세스 안에는 여러개의 스레드가 포함되어 있을 수 있다. 프로세스 내의 스레드들은 각자 독립적인 스택을 갖고있다.
Multithreaded Model을 사용하는 이유
단일 주소 공간에서 병렬 응용 가능
병령 프로세스를 사용하는 경우 주소 공간을 공유할 수 없다. 프로세스간 주소 공간을 공유하려면 특별한 방법이 필요하고 복잡해진다.
스레드 생성/ 삭제에 적은 오버헤드
스레드간에 공유되는 자원들이 존재하므로 추가 생성이 필요없다. 프로세스 생성은 Stack, data, text 모두 메모리를 할당해야 하지만 스레드는 Stack공간만 생성하면 되기 때문이다. 또한 스레드 생성은 프로세스 생성보다 10배~200배 정도 빠르다.
성능 측면 이유(CPU가 1개만 있는 경우라도)
CPU-bound(CPU를 사용하는 작업)와 I/O-bound(I/O를 하는 작업)이 혼재되어 있으면 성능상에서 이득이다.
CPU-bound만 있는 경우에는 성능 이득이 없다.
멀티 스레드 모델 예
워드프로세서
예를 들어 워드 프로세서가 하나의 스레드로 이루어져있다고 한다면 문서를 저장, 검색등을 하는 경우에 아무 일도 할 수 없다.
또한 입력을 받음과 동시에 문서를 화면에 바로 띄울 수도 없을 것이다.
워드 프로세서를 예를 들어 사용자에게 입력받는 스레드, 문서를 화면에 띄우는 스레드 문서를 디스크에서 가져오거나 저장하는 스레드가 있다고 한다면 각각 스레드의 작동은 스레드 별로 진행되기 때문에 입력을 받으면서 화면에 띄울수도, 저장을 하면서 문서를 수정할수도 있게된다. 또한 프로세스를 이용하는게 아니기 때문에 무거운 프로세스를 똑같이 여러개을 띄울 필요도 없다.
웹서버
멀티 스레드 모델의 예로 웹 서버를 들 수 있다.
웹서버는 Network connection에서 request를 수신하면 디스크에서 해당하는 웹 문서를 찾아서 돌려준다.
request가 수신된다면 Dispatcher thread에서 Worker thread로 일감을 나누어 주는 역할을 한다. 일꾼이 필요 한 만큼 Dispather thread는 스레드를 생성한다. Web page cache(개념적 캐시)는 자주 사용되는 웹 페이지 문서를 저장해놓는 개념적 캐시인데, Worker thread는 Web page cache에서 웹 문서를 찾고 없다면 디스크에서 html문서를 찾은 후 response 해준다. 또한 문서를 찾는 와중에도 다른 request가 수신된다면 다시 스레드를 생성하여 일꾼을 생성한다.
스레드 구현방식
User space에서 구현
- User space구현방식은 스레드 관련 sys call은 없다.
- 커널은 프로세스만을 인식하며 운영체제는 스레드의 존재를 모른다.
따라서 스레드는 어플리케이션의 일부가 된다.
- sys call을 이용하지 않기 때문에 오버헤드가 적지 않다 하지만 커널에서에 비하면 적다. 커널을 거치지 않기 때문이다
- 블록킹 sys call이 문제가 된다. 운영체제는 1개의 스레드인줄 알지만 프 로세스가 3개로 나누어 쓰는 것이다. 그렇기 때문에 3개중 1개의 스레드 만이 문제가 되도 3개의 스레드가 전부 block된다. 이런 문제 때문에 block이 되기 전에 검사를 하고 명령을 실행한다. 또한 sys call에서 블 록이 된다면 함수 자체에 덮어 씌운다.
- 커널 상에는 프로세스 테이블만이 존재를한다.
- 스레드 테이블은 프로세스 내부에 있다. 또한 프로세스가 스레드를 관리 한다.
만약 커널(운영체제)에 프로세스 A에 A스레드의 존재를 알고있다 해도 프로세스는 A1, A2, A3로 나누어 사용할 수 있다 하지만 운영체제는 그저 A라는 프로세스로만 알고있다.
스레드의 작업 스케쥴링을 A1, A2, A3는 가능하지만 A1, B1, A2는 불가능하다.
한 프로세스 안에서 스레드간의 전환은 RTS에 의해 이루어 진다.
Kernel Space에서 구현
- 커널을 거치기 때문에 오버헤드가 크다.
- 스레드 관련 sys call을 통해 이루어진다.
- 블록킹 sys call이 가능하다.
- 운영체제가 스레드의 존재를 알고있으며, 프로세스 테이블과 커널 테 이블이 모두 커널상에 존재한다.
- 커널이 스레드를 직접 선택하며 스케쥴링한다. 그렇기 때문에 스레드의 작업 진행을 A1, B2, A3, B2처럼 번갈아가며 스케쥴링 할 수 있다.
혼합형 구현방식
운영체제는 스레드의 존재를 알고있다 하지만 스레드 내부의 멀티 스레드는 모른다.
다수의 사용자 스레드를 커널 스레드에 매핑한다.
멀티 스레드 코드에서의 문제
만약 스레드1에서 Access호출을 하였을 때 에러가 발생하여 errno라는 변수에 에러 코드를 저장을 했다고 가정한다. 그 후 스레드 2에서 Open을 호출하였는데 또 다시 errno에 에러 코드를 overwritten을 한다. 이 후 스케쥴러에 의해 다시 errno를 스레드1이 접근할 경우 전혀 다른 에러코드가 스레드1에 알려진다. 전역변수를 공유하기 때문에 스레드간의 충돌이 발생하고, 전혀 다른 에러코드를 다른 스레드가 받게 될 수 있다.
전역변수 충돌이란 문제점을 개별 전역 변수 사용으로 문제를 해결할 순 있겠지만 이는 깔끔한 방법이 아니다. 스레드간의 안정을 책임지는 코드와, 재진입 가능 코드를 추가해야 한다.
만약 어떤 스레드에 의해 실행중인 프로시져가 다른 스레드에 의해 재 진입 된다면? 라이브러리 프로시져들은 재진입이 가능해야 한다.
또한 시그널을 핸들링 해야한다. 시그널 핸들링은 어떤 스레드가 수행해야 하는가. 새로운 스레드를 생성하여 수행해야 하는가.
이는 IPC(Inter-Process Communication)과 ITC(Inter-Thread Communication)에서 다룬다.
스레드 실행 예제
1. thr1
#include <pthread.h>
#include <stdio.h>
#include<unistd.h>
void *func(void *i);
int main(void)
{
pthread_t thr;
printf("\n");
pthread_create(&thr, NULL, func, NULL);
printf("Main thread ...\n");
pthread_join(thr, NULL);
printf("Joined ...\n");
}
void *func(void *arg)
{
printf("Child thread ...\n");
sleep(60);
pthread_exit(NULL);
}
해당 프로그램은 부모 스레드가 자식 스레드를 create한다. 자식 스레드는 func함수를 실행하게 되며 print를 한 후 sleep를 60초 동안 한 후 종료된다. 이 후에 부모스레드는 pthread_join으로 자식 스레드의 종료를 기다렸다가 하나의 스레드로 합쳐지고, Joined...를 출력하고 종료된다.
ps
ps -L
실행결과:
1개의 프로세스가 생성된다.
2개의 스레드가 수행된다.
이 때 ps -L을 실행하게 되면 LWP라는게 등장하게 되는데 LWP는 Light Weight Process이며 PID는 Process ID이다.
2. thr2
#include <pthread.h>
#include <stdio.h>
void *X(void *p);
void Y(int j);
int global = 0;
int main()
{
pthread_t t1;
void *status;
printf("\n");
pthread_create(&t1, NULL, X, NULL);
Y(1);
pthread_join(t1, &status);
printf("global=%d in main thread after the child thread exiting\n", global);
}
void *X(void *p)
{
int i;
do
{
global = 2;
for (i = 0; i < 100000; i++)
; printf("Child thread: global = %d \n", global);
} while (1);
pthread_exit((void *)NULL);
}
void Y(int j)
{
int i;
do
{
global = j;
for (i = 0; i < 100000; i++)
;
printf("Main thread: global=%d\n", global);
} while (1);
}
global이란 전역변수 메모리 공간이 있다. 또한 x라는 함수는 global에 2를 저장하고 Y라는 함수는 global에 1을 대입하며 각 각 출력한다.
x는 자식 스레드가 실행하고
Y는 부모 스레드가 실행한다.
그렇다면 출력의 결과는 Child thread: global = 2, Mainthread: global = 1이 출력되어야 할 것이다. 하지만 그렇지 않다.
실행의 결과는 서로가 섞여서 나오게 된다. 이는 스레드간의 전역변수 메모리 영역을 공유한다는 의미이다.