TCP/IP 쓰레드의 이론적 이해
쓰레드의 이론적 이해
쓰레드의 등장배경
프로세스는 부담스럽다.
- 프로세스의 생성에는 많은 리소스가 소모된다.
- 일단 프로세스가 생성되면, 프로세스간의 컨텍스트 스위칭으로 인해서 성능이 저하된다.
- 컨텍스트 스위칭은 프로세스이 정보를 하드드시크에 저장 및 복원하는 일이다.
데이터의 교환이 어렵다.
- 프로세스간 메모리가 독립적으로 운영되기 때문에 프로세스간 데이터 공유 불가능!
- 따라서 운영체제가 별도로 제공하는 메모리 공간을 대상으로 별도의 IPC 기법 적용.
그렇다면 쓰레드는?
- 프로세스보다 가벼운, 경량화된 프로세스이다. 때문에 컨텍스트 스위칭이 빠르다.
- 쓰레드 별로 메모리 공유가 가능하기 때문에 별도의 IPC 기법 불필요.
- 프로세스 내에서의 프로그램의 흐름을 추가한다.
쓰레드와 프로세스의 차이점
프로세스는 서로 완전히 독립적이다. 프로세스는 운영체제 관점에서의 실행흐름을 구성한다.
쓰레드는 프로세스 내에서의 실행흐름을 갖는다. 그리고 데이터 영역과 힙영역을 공유하기 때문에 컴텍스트 스위칭에 대한 부담이 덜하다. 또한 궁유하는 메머리 영역으로 인해서 쓰레드간 데이터 교환이 매우 쉽게 이뤄진다.
운영체제와 프로세스, 쓰레드의 관계
하나의 운영체제 내에서는 둘 이상의 프로세스가 생성되고, 하나의 프로세스 내에서는 둘 이상의 쓰레드가 생성된다.
쓰레드의 생성 및 실행
쓰레드의 생성과 실행흐름의 구성
#include<pthread.h>
int pthred_create(
pthread_t *restrict thread, const pthread_attr_t *restrict attr,
void *(start_routine)(void*_, void *restrict arg
);
성공시 0, 실패시 0이외의 값 반환
thread : 생성할 쓰레드의 ID저장을 위한 변수의 주소 값 전달, 참고로 쓰레드는 프로세스와 마찬가지로 쓰레드의 구분을 위한 ID가 부여된다.
attr : 쓰레드에 부여할 특성 정보의 전달을 위한 매개변수, NULL전달 시 기본적인 특성의 쓰레드가 생성된다.
start_routine : 쓰레드의 main함수 역할을 하는, 별도 실행흐름의 시작이 되는 함수의 주소값(함수 포인터)전달.
arg : 세번째 인자를 통해 등록된 함수가 호출될 때 전달할 인자의 정보를 담고 있는 변수의 주소 값 전달
쓰레드 생성의 예
#include <stdio.h>
#include <pthread.h>
void* thread_main(void *arg);
int main(int argc, char *argv[])
{
pthread_t t_id;
int thread_param=5;
if(pthread_create(&t_id, NULL, thread_main, (void*)&thread_param)!=0)
{
puts("pthread_create() error");
return -1;
};
sleep(10); puts("end of main");
return 0;
}
void* thread_main(void *arg)
{
int i;
int cnt=*((int*)arg);
for(i=0; i<cnt; i++)
{
sleep(1); puts("running thread");
}
return NULL;
}
해설
thread_main함수의 호출을 시작으로 별도의 실행흐름을 구성하는 쓰레드의 생성을 요청하고 있다. 더불어 thread_main함수호출 시 인자로 변수 thread_param의 주소 값을 전달하고 있다.
sleep 함수의 호출을 통해서 main함수의 실행을 10초간 중지시키고 있다. 이는 프로세스의 종료시기를 늦추기 위함이다. 16행의 return문이 실행되면 프로세스는 종료된다. 그리고 프로세스의 종료는 그 안에서 생성된 쓰레드의 종료로 이어진다. 따라서 쓰레드의 실행을 보장하기 위해서 이 문장이 삽입되었다.
매개변수 arg로 전달되는 것은 10행에서 호출한 pthread_create 함수의 네번째 전달인자이다.
실행결과
-lpthread 옵션을 추가하여 쓰레드 라이브러리 링크 별도 지시
프로세스의 종료와 쓰레드
쓰레드의 종료를 대기
#include <pthread.h>
int pthread_join(pthread_t thread, void **status);
성공 시 0, 실패시 0 이외의 값 반환
thread : 이 매개변수에 전달되는 ID의 쓰레드가 종료될 때 까지 함수는 반환되지 않는다.
status : 쓰레드의 main함수가 반환하는 값이 저장될 포인터 변수의 주소 값을 전달한다.
첫 번째 인자로 전달되는 ID의 쓰레드가 종료될 때 까지, 이 함수를 호출한 프로세스(또는 쓰레드)를 대기상태에 둔다.
ptherad_join 함수의 호출 예
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void* thread_main(void *arg);
int main(int argc, char *argv[])
{
pthread_t t_id;
int thread_param=5;
void * thr_ret;
if(pthread_create(&t_id, NULL, thread_main, (void*)&thread_param)!=0)
{
puts("pthread_create() error");
return -1;
};
if(pthread_join(t_id, &thr_ret)!=0)
{
puts("pthread_join() error");
return -1;
};
printf("Thread return message: %s \n", (char*)thr_ret);
free(thr_ret);
return 0;
}
void* thread_main(void *arg)
{
int i;
int cnt=*((int*)arg);
char * msg=(char *)malloc(sizeof(char)*50);
strcpy(msg, "Hello, I'am thread~ \n");
for(i=0; i<cnt; i++)
{
sleep(1); puts("running thread");
}
return (void*)msg;
}
해설
19행 : main함수에서, 13행에서 생성한 쓰레드를 대상으로 pthread_join함수를 호출하고 있다. 때문에 main함수는 변수 t_id에 저
장된 ID의 쓰레드가 종료될 때 까지 대기하게 된다.
11, 19, 41행 : 이 세 문장을 통해서 쓰레드가 반환하는 값이 참조되는 방법을 이해해야한다.
간단히 설명하면, 41행에 의해서 반환되는 ㄱ밧은 19행의 두 번째 인자로 전달된 변수 thr_ret에 저장된다.
이 반환 값은 thread_main함수 내에서 동적으로 할당된 메모리 공간의 주소 값이라는 사실도 알아야 한다.
실행결과
임계영역 내에서 호출이 가능한 함수
쓰레드에 안전한 함수, 쓰레드에 불안전한 함수
- 둘 이상의 쓰레드가 동시에 호출하면 문제를 일으키는 함수를 가리켜 쓰레드에 불안전한 함수(Thread-safe function)라 한다.
- 둘 이상의 쓰렉드가 동시에 호출해도 문제를 일으키지 않는 함수를 기리켜 쓰레드에 안전한 함수(Thread-unsafe function)라 한다.
쓰레드에 안전한 함수의 예
struct hostent *gethostbyname(const char *hostname); 불안전
struct hostent *gethostbyname_r(const char *name, struct hostent *result, char *buffer, intbuflen, int *h_errnop); 안전
헤더파일 선언 이전에 매크로 _REENTRANT를 정의하면, 쓰레드에 불안전한 함수의 호출문을 쓰레드에 안전한 함수의 호출문으로 자동 변경 컴파일 된다.
워커(Worker) 쓰레드 모델
워커(Worker) 쓰레드 모델의 예
#include <stdio.h>
#include <pthread.h>
void * thread_summation(void * arg);
int sum=0;
int main(int argc, char *argv[])
{
pthread_t id_t1, id_t2;
int range1[]={1, 5};
int range2[]={6, 10};
pthread_create(&id_t1, NULL, thread_summation, (void *)range1);
pthread_create(&id_t2, NULL, thread_summation, (void *)range2);
pthread_join(id_t1, NULL);
pthread_join(id_t2, NULL);
printf("result: %d \n", sum);
return 0;
}
void * thread_summation(void * arg)
{
int start=((int*)arg)[0];
int end=((int*)arg)[1];
while(start<=end)
{
sum+=start;
start++;
}
return NULL;
}
해설
두 쓰레드가 하나의 전역변수 sum에 직접 접근한다. 따라서 문제의 발생소지를 지니고 있는 상황이다.
실행결과 :thread3.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREAD 100
void * thread_inc(void * arg);
void * thread_des(void * arg);
long long num=0;
int main(int argc, char *argv[])
{
pthread_t thread_id[NUM_THREAD];
int i;
printf("sizeof long long: %d \n", sizeof(long long));
for(i=0; i<NUM_THREAD; i++)
{
if(i%2)
pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
else
pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
}
for(i=0; i<NUM_THREAD; i++)
pthread_join(thread_id[i], NULL);
printf("result: %lld \n", num);
return 0;
}
void * thread_inc(void * arg)
{
int i;
for(i=0; i<50000000; i++)
num+=1;
return NULL;
}
void * thread_des(void * arg)
{
int i;
for(i=0; i<50000000; i++)
num-=1;
return NULL;
}
해설
총 100개의 쓰레드를 생성해서 그 중 반은 thread_inc를 쓰레드의 main함수로, 나머지 반은 thread_des를 쓰레드의 main함수로 호출하게 하고 있다. 이로써 전역변수 num에 저장된 값의 증가와 감소의 최종결과로 변수 num에는 0이 저장되어야 한다.
실행결과 : thread4.c
쓰레드의 문제점과 임계영역(Critical Section)
하나의 변수에 둘 이상의 쓰레드가 동시에 접근하는 것이 문제
임계영역은 어디?
void * thread_inc(void *arg)
{
int i;
for(i=0; i<5000000; i++)
num+=1; //임계영역
return NULL;
}
void * thread_des(void * arg)
{
int i;
for(i=0; i<5000000; i++)
num+=1; //임계영역
return NULL;
}
임계역역은 둘 이상의 쓰레드가 동시에 실행하면 문제를 일으키는 영역이다. 왼쪽에서 보이는 바와 같이, 서로 다른 문장임에도 불구하고 당시에 실행이 되는 상황에서도 문제는 발생할 수 있기 때문에 임계영역은 다양하게 구성이 된다.
쓰레드의 동기화
동기화의 두 가지 측면과 동기화 기법
동기화가 필요한 대표적인 상황
- 동일한 메모리 영역으로의 동시접근이 발생하는 상황
- 동일한 메모리 영역에 접근하는 쓰레드의 실행순서를 지정해야 하는 상황
즉, 동기화를 통해서 동시접근을 막을 수 있고, 게다가 접근의 순서를 지정하는 것도 가능하다.
동기화 기법
- 뮤텍스(Mutex)기반 동기화
- 세마포어(Semaphore)기반 동기화
동기화는 운영체제가 제공하는 기능이기 때문에 운영체제에 따라서 제공되는 기법 및 적용의 방법에 차이가 있다.
뮤텍스 기반의 동기화
뮤텍스란?
쓰레드의 동시접근을 허용하지 않는다는 의미.
뮤택스의 생성과 소멸
#include <pthrea.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
성공 시 0, 실패시 0 이외의 갑 반환.
mutex : 뮤텍스 생성시에는 뮤텍스의 참조 값 저장을 위한 변수의 주소 값 전달, 그리고 뮤텍스 소멸 시에는 소멸하고자 하는 뮤텍스의 참조 값을 저장하고 있는 변수의 주소 값 전달.
attr : 생성하는 뮤텍스의 특성정보를 담고 있는 변수의 주소 값 전달, 별도의 특성을 지정하지 않을 경우에는 NULL전달.
뮤텍스의 획득과 반환
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
성공 시 0, 실패시 0 이외의 값 반환
뮤텍스 기반 동기화의 기본구성
pthread_mutex_lock(&mutex);
//임계영역의 시작
// . . . . .
//임계영역의 끝
pthread_mutex_unlock(&mutex);
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREAD 100
void * thread_inc(void * arg);
void * thread_des(void * arg);
long long num=0;
pthread_mutex_t mutex;
int main(int argc, char *argv[])
{
pthread_t thread_id[NUM_THREAD];
int i;
pthread_mutex_init(&mutex, NULL);
for(i=0; i<NUM_THREAD; i++)
{
if(i%2)
pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
else
pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
}
for(i=0; i<NUM_THREAD; i++)
pthread_join(thread_id[i], NULL);
printf("result: %lld \n", num);
pthread_mutex_destroy(&mutex);
return 0;
}
void * thread_inc(void * arg)
{
int i;
pthread_mutex_lock(&mutex);
for(i=0; i<50000000; i++)
num+=1;
pthread_mutex_unlock(&mutex);
return NULL;
}
void * thread_des(void * arg)
{
int i;
for(i=0; i<50000000; i++)
{
pthread_mutex_lock(&mutex);
num-=1;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
/*
swyoon@com:~/tcpip$ gcc mutex.c -D_REENTRANT -o mutex -lpthread
swyoon@com:~/tcpip$ ./mutex
result: 0
*/
세마포어(Semaphore)
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
성공시 0, 실패시 0이외의 값 반환.
sem : 세마포어 생성시에는 세미포어의 참조 값 저장을 위한 변수의 주소 값 전달, 그리고 세마포어 소멸 시에는 소멸하고자 하는 세마포어의 참조 값을 저장하고 있는 변수의 주소 값 전달.
pshared : 0이외의 값 전달 시, 둘 이상의 프로세스에 의해 접근 가능한 세미포어 생성, 0 전달시 하나의 프로세스 내에서만 접근 가능한 세미포어 생성, 우리는 하나의 프로세스 내에 존재하는 쓰레드의 동기화가 목적이므로 0을 전달한다.
value : 생성되는 세마포어의 초기 값 지정
#include <semphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
성공시 0, 실패시 0 이외의 값 반환
sem : 세미포어의 참조 값을 저장하고 있는 변수의 주소 값 전달, sem_post에 전달되면 세미포어의 값은 하나 증가, sem_wait에 전달되면 세미포어의 값은 하나 감소.
세마포어 기반 동기화의 예
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
void * read(void * arg);
void * accu(void * arg);
static sem_t sem_one;
static sem_t sem_two;
static int num;
int main(int argc, char *argv[])
{
pthread_t id_t1, id_t2;
sem_init(&sem_one, 0, 0);
sem_init(&sem_two, 0, 1);
pthread_create(&id_t1, NULL, read, NULL);
pthread_create(&id_t2, NULL, accu, NULL);
pthread_join(id_t1, NULL);
pthread_join(id_t2, NULL);
sem_destroy(&sem_one);
sem_destroy(&sem_two);
return 0;
}
void * read(void * arg)
{
int i;
for(i=0; i<5; i++)
{
fputs("Input num: ", stdout);
sem_wait(&sem_two);
scanf("%d", &num);
sem_post(&sem_one);
}
return NULL;
}
void * accu(void * arg)
{
int sum=0, i;
for(i=0; i<5; i++)
{
sem_wait(&sem_one);
sum+=num;
sem_post(&sem_two);
}
printf("Result: %d \n", sum);
return NULL;
}
14, 15행 : 세마포어를 두개 생성하고 있음. 하나는 세마포어 값이 0이고, 다른 하나는 1이 두개의 세마포아가 필요한 이유를 알아야 함.
35, 48행 : 세마포어 변수 sem_two를 이용한 wait와 post함수의 호출. 이는 accu함수를 호출하는 쓰레드가 값을 가져가지도 않는데, read함수를 호출하는 쓰레그다 값을 다시 가져다 놓는 상황을 막기 위함.
37, 46행 : 세마포어 변수 sem-one을 이용한 wait과 post 함수의 호출. read함수를 호출하는 스레드가 새로운 값을 가져다 놓기도 전에 accu함수가 값을 가져가 버리는 (이전 값을 다시 가져가는) 상환을 막기 위함이다.
쓰레드의 소멸과 멀티쓰레드 기반의 다중접속 서버의 구현
쓰레드를 소멸하는 두가지 방법
#include <pthread.h>
int pthread_detach(pthread_t thread);
성공 시 0, 실패시 0 이외의 값 반환
thread 종료와 동시에 소멸시킬 쓰레드의 ID정보 전달.
pthread_join 함수의 호출은 블로킹 상태에 놓이게 되니 pthread_detach함수를 호출해서 쓰레드의 소멸을 도와야 한다.
멀티쓰레드 기반의 다중접속 서버의 구현
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#define BUF_SIZE 100
#define MAX_CLNT 256
void * handle_clnt(void * arg);
void send_msg(char * msg, int len);
void error_handling(char * msg);
int clnt_cnt=0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
int clnt_adr_sz;
pthread_t t_id;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
pthread_mutex_init(&mutx, NULL);
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
while(1)
{
clnt_adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
pthread_mutex_lock(&mutx);
clnt_socks[clnt_cnt++]=clnt_sock;
pthread_mutex_unlock(&mutx);
pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);
pthread_detach(t_id);
printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));
}
close(serv_sock);
return 0;
}
void * handle_clnt(void * arg)
{
int clnt_sock=*((int*)arg);
int str_len=0, i;
char msg[BUF_SIZE];
while((str_len=read(clnt_sock, msg, sizeof(msg)))!=0)
send_msg(msg, str_len);
pthread_mutex_lock(&mutx);
for(i=0; i<clnt_cnt; i++) // remove disconnected client
{
if(clnt_sock==clnt_socks[i])
{
while(i++<clnt_cnt-1)
clnt_socks[i]=clnt_socks[i+1];
break;
}
}
clnt_cnt--;
pthread_mutex_unlock(&mutx);
close(clnt_sock);
return NULL;
}
void send_msg(char * msg, int len) // send to all
{
int i;
pthread_mutex_lock(&mutx);
for(i=0; i<clnt_cnt; i++)
write(clnt_socks[i], msg, len);
pthread_mutex_unlock(&mutx);
}
void error_handling(char * msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
해석
9, 10행 : 서버에 접속한 클라잉언트의 소켓 관리를 위한 변수와 배열이다. 이 둘의 접근과 관련 있는 코드가 임계영역을 구성하게 됨에 주의하자.
43행 : 새로운 연결이 형성될 때 마다 변수 clont_cnt와 배열 clnt_socks에 해당 정보를 등록한다.
47행 : pthread_detach 함수 호출을 통해서 종료된 쓰레드가 메모리에서 완전히 소멸되도록 하고 있다.
78행 : 이 함수는 연결된 모든 클라이언트에게 메시지를 저송하는 기능을 제공한다.
실행결과