2015년 1월 7일 수요일

TCP/IP 소켓 프로그래밍 18강

파트2의 마지막 강인 18강이다.(이런 18...;;)
2015년이 되니 하고 싶은 것도 많고 할일도 벌려놔서 소켓 프로그래밍 공부가 더뎌지고 있다 ;;
소켓 프로그래밍도 몇강 남지 않았다.
이번 1월 안에 끝내보자~ ^^
18강은 멀티쓰레드 기반의 서버 구현이다.
전에 멀티프로세스는 해봤지만 쓰레드는 처음이다.

18-1 : 쓰레드의 이론적 이해


쓰레드의 등장 배경은 멀티프로세스의 단점으로 인해 등장했다.

-멀티 프로세스의 단점-
1.프로세스의 생성이라는 과부하 걸리는 작업과정이 필요하다
2. 두 프로세스 사이에서의 데이터 교환을 위해서는 별도의 IPC 기법을 적용해야한다.

이런 단점을 극복하기 위해 쓰레드가 등장했다.

-쓰레드의 장점-
1. 쓰레드의 생성 및 컨텍스 스위칭(프로세서가 실행되기 위해 메모리에 올리고 내리는 방식)은 프로세스의 생성 및 컨텍스 스위칭보다 빠르다
2. 쓰레드 사이에서의 데이터 교환에는 특별한 기법이 필요치 않다.

쓰레드와 프로세스의 차이점
이건 말보다는 그림으로 보는 것이 이해가 빨라서 그림을 첨부한다.
(출처 - 윤성우 열혈 TCP/IP 소켓 프로그래밍)


위 그림은 프로세서의 메모리 상태이다.
프로세서는 전체를 통으로 복사해서 사용하기에 메모리가 저리 각자 할당된다.
그러다 보니 메모리 용량이 커지고, 메모리에 올렸다 내렸다 하는 과정
(고급스럽게 컨텍스 스위칭 이라고 한다.)이 느려진다.



그래서 쓰레드에서는 위의 그림같이 힙과 데이터 영역은 공유하고, 스택만 독립했다.
스택 영역은 작은 메모리를 차지하기에 컨텍스 스위칭을 할 때 부담이 적다.
그래서 빠른 것이다.

18-2 : 쓰레드의 생성 및 실행


쓰레드의 생성과 실행 흐름의 구성

쓰레드는 별도의 실행흐름을 갖기 때문에 쓰레드만의 main 함수를 별도로 지정해야한다.
그리고, 별도의 실행흐름을 형성해 줄 것을 os에게 요청해야 하는데 그 함수는 다음과 같다.

#include <pthread.h>
int pthread_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;

//thread_main 함수의 호출을 시작으로 별도의 실행흐름을 구성하는 스레드의 생성을 요청
if(pthread_create(&t_id, NULL, thread_main, (void*)&thread_param)!=0)
{
puts("pthread_create() error");
return -1;
};
//main함수의 실행을 10초간 중지시킨다. 프로세스의 종료시기를 늦추기 위함  
sleep(10);  puts("end of main");
return 0;
}
//매개변수 arg로 전달되는 것은 pthread_create 함수의 네 번째 전달 인자다.
void* thread_main(void *arg)
{
int i;
int cnt=*((int*)arg);
for(i=0; i<cnt; i++)
{
sleep(1);  puts("running thread");
}
return NULL;
}

쓰레드 관련코드 컴파일 할 때는 -lpthread 옵션을 추가해서 해야한다.

예) /tcpip# gcc thread1.c -o tr1 -lpthread

하지만 위의 소스처럼 sleep를 이용해서 흐름을 제어하는 경우는 거의 없다.
언제 쓰레드가 종료될지 예측한다는 것은 어렵기 때문이다.
그래서 쓰레드의 흐름을 조절하는 함수를 사용해야 한다.

#include <pthread.h>
int pthread_join(pthread_t thread, void **status);
-> 성공시 0, 실패 시 0 이외의 값 반환
-thread : 이 매개변수에 전달되는 ID의 쓰레드가 종료될 때까지 함수는 반환하지 않는다.
-status : 쓰레드의 main 함수가 반환하는 값이 저장될 포인터 변수의 주소 값을 전달한다.

소스로 확인해보자.

#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;
}

이번에는 쓰레드를 둘 이상을 생성해서 쓰는 것을 알아보려고 한다.
쓰레드를 2개 이상 사용하는 경우 주의할 점이 있다.
쓰레드 2개가 서로 영향을 끼치지 않는 경우는 상관이 없지만.
만약 데이터를 공유해서 값을 바꿔야 하는 경우에는 예기치 못하게 스탭이 꼬여서(?) 엉뚱한 값으로 변할 수 있다.
기본적으로 쓰레드에 안전한 함수는 표준 함수들이다.
하지만 사용자가 만든 함수나 쓰레드의 영향을 받는 데이터 계산 부분은 불안전한 부분이다.
이런 불안전한 부분을 고급스럽게(?) 임계영역이라고 한다.
일반적으로 쓰레드에 안전한 형태로 재 구현된 함수의 이름에는 _r이 붙는다.(리눅스기준)
_r이 붙은 함수를 사용할 땐 컴파일 시 -D_REENTRANT 의 옵션을 추가해야한다.

예) gcc -D_REENTRANT mythread.c -o mthread -lpthread

또 다른 소스를 보자.
이 소스는 두개의 쓰레드를 생성해서 하나는 1~5를 더하고,
다른 하나는 6~10까지 더하는 것이다.

#include <stdio.h>
#include <pthread.h>
void * thread_summation(void * arg);
//이 전역변수를 이용해서 데이터를 공유해가며 계산할 것이다.
int sum=0;

int main(int argc, char *argv[])
{
//쓰레드 2개를 넣을 변수
pthread_t id_t1, id_t2;
int range1[]={1, 5};
int range2[]={6, 10};
//쓰레드 2개 생성
pthread_create(&id_t1, NULL, thread_summation, (void *)range1);
pthread_create(&id_t2, NULL, thread_summation, (void *)range2);
//쓰레드 2개가 종료될 때까지 기다림
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;
}

실행을 해보면 그나마(?) 잘 계산이 된다.
그럼 더 잘 꼬이도록 짝수, 홀수 만큼 1씩 더했다 뺏다를 해보겠다.
소스를 보자.

#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;
}

제대로 잘 돌아갔다면 답은 0이 나와야 한다.
실행해보면 재미있는 결과를 알 수 있다. ㅡ.ㅡ
왜 이런 비극(?)이 일어났을까??

18-3 : 쓰레드의 문제점과 임계영역


위에 잠깐 설명했던 임계영역을 서로 마구 침범(?) 했기 때문에 문제가 발생한 것이다.
이런 일이 일어나는 이유는 cpu 때문이다.
cpu는 항상 바쁘다. 그러다 보니 어떤 것이 들어오면 순차적으로 처리하는 것이 아닌
엄청 빠른 시간으로 왔다갔다하며 실행을 하는 것이다.
그러다 보면 당연 쓰레드 같은 것도 스텝이 꼬여서 실행시키는 것이다.
문제가 발생하는 상황을 위의 소스에서 세가지 형태로 나눠서 정리할 수 있다.

1. 두 쓰레드가 동시에 thread_inc 함수를 실행하는 경우
2. 두 쓰레드가 동시에 thread_des 함수를 실행하는 경우
3. 두 쓰레드가 각각 thread_inc,  thread_des  함수를 동시에 실행하는 경우
이런 비극(?)을 방지하는 법을 쓰레드 동기화 라고 한다.

18-4 : 쓰레드 동기화


쓰레드 동기화는 쓰레드의 접근순서 때문에 발생하는 문제점의 해결책을 뜻한다.
동기화가 필요한 상황은 두가지로 생각해볼 수 있다.
1. 동일한 메모리 영역으로의 동시 접근이 발생한 경우
2. 동일한 메모리 영역에 접근하는 쓰레드의 실행 순서를 지정해야 하는 상황

동기화 기법은 뮤텍스, 세마포어 방식이 있다.

뮤텍스(Mutual Exclusion의 줄임말로 쓰레드의 동시 접근을 허용하지 않는다는 의미다
뮤텍스 생성/소멸 함수를 보자

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); (생성)
int pthrread_mutrx_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이외의 값 반환

소스를 보고 판단해보자.

#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;
}

이번에는 세마포어 생성/소멸 함수를 보자.

#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 <semaphore.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);
//쓰레드 2개 생성
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 : 쓰레드의 소멸과 멀티쓰레드 기반의 다중접속 서버의 구현


이제는 쓰레드를 소멸 시키는 법도 알아야 한다.
쓰레드를 소멸하는 방법은 2 가지가 있다.(근데 책에는 3가지라고 제목이 되어있네?!)
1. pthread_join 함수의 호출
2. pthread_detach 함수의 호출

1번은 우리가 앞에서 몇번 쓴 함수다. 하지만 이 함수는 쓰레드가 종료될 때까지 대기 상태에 놓이게 된다는 것이다. 효율성에 문제가 있다.

그래서 주로 2번 방식을 쓴다고 한다.

#include <pthread.h>
int pthread_detach(pthread_t thread);
-> 성공시 0, 실패시 0 이외의 값 반환
-thread 종료와 동시에 소멸시킬 쓰레드의 ID 정보 전달

위 함수를 호출했다고 즉시, 종료되거나 블로킹 상태에 놓이지는 않는다. 
하지만 메모리의 소멸을 유도할 수 있다.

이번 강은 무지하게 길었다 -_-;; 지금도 엄청 줄이고 줄인 것이다.
역시 직접 소스를 다운 받아 해보는 것이 더욱 중요하다.

댓글 없음:

댓글 쓰기