[열혈TCP/IP] 18 멀티쓰레드 기반의 서버 구현
18 멀티쓰레드 기반의 서버 구현
쓰레드를 사용하는 이유 = 멀티프로세스 기반의 단점
- 프로세스 생성이 무거운 과정이다.
- IPC를 사용해서 프로세스간 통신을 하는 것이 복잡하다.
- context switching에 따른 부담이 굉장히 크다. = 둘 이상의 실행흐름을 갖기 위해서 프로세스가 유지하고 있는 메모리 영역을 통째로 복사한는 과정이 부담스럽다
쓰레드와 프로세스의 차이점
- 프로세스 : Text Data Heal Stack에서 DHS가 모두 독립적으로 구성됨
- 운영체제 관점에서 별도의 실행흐름을 구성하는 단위
- 쓰레드 : Data 영역과 Heap 영역을 공유함
- 프로세스 관점에서 별도의 실행흐름을 구성하는 단위
18-2 쓰레드의 생성및 실행
pthread_create()을 사용한다.
#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;
}
만약 main 의 sleep(10)을 sleep(2)로 바꾸면 메인이 종료되면서 Thread가 다 죽기 때문에 Thread가 5초동안 다 돌지 않는다. thread가 종료되는 것을 일일이 예측할 수 없기 때문에 pthread_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;
}
여러 개의 thread가 동시에 접근하는 메모리는 thread_safe하지 않는다. 일반적으로 구현된 함수들 중에서도 안전하지 않은 함수가 있는데, 이런 위험한 함수들을 자동으로 안전한 함수로 바꿔주기 위해서 컴파일을 할 때 아래와 같이 한다.
gcc -D_REENTRANT mythread -o mthread -lpthread
따라서 앞으로 쓰레드 관련 코드가 삽입되어 있는 예제를 컴파일 할 때는 -D_REENTRANT 옵션을 항상 추가한다.
아래는 두 개의 쓰레드로 1~10의 합을 나눠서 진행하는 코드이다.
#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;
}
위 예제는 55가 잘 출력되지만 아래의 예제는 결과가 0이 출력되어야 하는데 엉뚱한 값이 나온다.
#include <stdio.h>
#include <windows.h>
#include <process.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[])
{
HANDLE 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;
}
이는 다음 절에서 해결할 수 있는 문제이다.
18-3 쓰레드의 문제점과 임계영역
하나의 변수에 둘 이상의 쓰레드가 동시에 접근하는 것이 문제이다. 값의 증가는 CPU가 처리하는 연산이고 이를 위해서 Thread는 변수의 값을 직접 증가시키는 것이 아니라 일단 참조하고 -> 값을 변화시키고 -> 다시 할당하는 과정을 거치기 때문에 이 순서가 mutex를 통해서 보장되어야 한다.
18-4 쓰레드 동기화
뮤텍스는 화장실 사용방법에서 자물쇠의 역할을 한다.
- 화장실의 접근보호를 위해서 들어갈 때 문을 잠그고 나올 때는 문을 연다.
- 화장실이 사용 중이라면 밖에서 대기한다.
- 대기중인 사람이 둘 이상이 될 수 있고, 이들은 대기순서에 따라서 화장실에 들어간다.
pthread_mutex_init()
과 pthread_mutex_destroy()
를 초기화와 소멸에 사용된다.
pthread_mutex mutex;
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
*/
임계 영역을 [0,50000000]의 합이 진행되는 전체 영역을 넓게 감싸는데 이 동안에는 다른 Thread는 돌지 못한다. 임계 영역의 범위는 상황에 따라서 잘 조절한다.
세마포어 Semaphore
바이너리 세마포로 설명을 진행한다. sem_init()
함수가 호출되면 운영체제에 의해서 세마포어 오브젝트가 생성된다. 이곳에는 세마포어 값이라 불리는 정수가 하나 기록된다. 이 값은 sem_post
가 호출되면 +1되고 sam_wait
가 호출되면 -1이 된다. 하지만 세마포어 값은 0보다 작아질 수 없기 때문에 현재 0인 상태에서 sem_wait가 호출되면 호춣나 쓰레드는 함수가 반환되지 않아서 블로킹 사태에 들어간다. 다른 쓰레드가 sem_post를 호출하면 값이 1로 바뀌고, 이 값으 1에서 0으로 바구면서 블로킹 상태에서 빠져나간다.
sem_wait(&sem);
/*
임계 영역
*/
sem_post(&sem);
아래의 코드는 “쓰레드 A가 프로그램 사용자로부터 값을 입력 받아서 전역변수 num에 저장하면, 쓰레드 B는 이 값을 가져다가 누적해 나간다. 이 과정은 총 5회 진행이 되고, 진행이 완료되면 총 누적 금액을 출력하면서 프로그램이 종료한다.
//semaphore.c
#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;
}
18-5 쓰레드의 소멸과 멀티쓰레드 기반의 다중접속 서버의 구현
쓰레드를 소멸하는 두 가지 방법
pthread_join()으로 쓰레드를 소멸시킬 수 있지만 이 함수는 쓰레드가 종료될 때까지 블로킹 상태에 놓이게 된다는 문제가 있다.
pthraed_detach() : 성공시 0, 실패시 0이외의 값 반환을 사용하는 것이 더 낫다.