[열혈TCP/IP] 18 멀티쓰레드 기반의 서버 구현

4 minute read

18 멀티쓰레드 기반의 서버 구현

쓰레드를 사용하는 이유 = 멀티프로세스 기반의 단점

  1. 프로세스 생성이 무거운 과정이다.
  2. IPC를 사용해서 프로세스간 통신을 하는 것이 복잡하다.
  3. context switching에 따른 부담이 굉장히 크다. = 둘 이상의 실행흐름을 갖기 위해서 프로세스가 유지하고 있는 메모리 영역을 통째로 복사한는 과정이 부담스럽다

쓰레드와 프로세스의 차이점

  1. 프로세스 : Text Data Heal Stack에서 DHS가 모두 독립적으로 구성됨
    • 운영체제 관점에서 별도의 실행흐름을 구성하는 단위
  2. 쓰레드 : 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 쓰레드 동기화

뮤텍스는 화장실 사용방법에서 자물쇠의 역할을 한다.

  1. 화장실의 접근보호를 위해서 들어갈 때 문을 잠그고 나올 때는 문을 연다.
  2. 화장실이 사용 중이라면 밖에서 대기한다.
  3. 대기중인 사람이 둘 이상이 될 수 있고, 이들은 대기순서에 따라서 화장실에 들어간다.

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

멀티쓰레드 기반의 다중접속 서버의 구현