[열혈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이외의 값 반환을 사용하는 것이 더 낫다.