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

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

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

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

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

  1. 프로세스 : Text Data Heal Stack에서 DHS가 모두 독립적으로 구성됨
    • 운영체제 관점에서 별도의 실행흐름을 구성하는 단위
  2. 쓰레드 : Data 영역과 Heap 영역을 공유함
    • 프로세스 관점에서 별도의 실행흐름을 구성하는 단위

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

pthread_create()을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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()을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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의 합을 나눠서 진행하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#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이 출력되어야 하는데 엉뚱한 값이 나온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#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()를 초기화와 소멸에 사용된다.

1
2
3
4
5
6
pthread_mutex mutex;
pthread_mutex_lock(&mutex);
/*
임계영역
*/
pthread_mutex_unlock(&mutex);

이를 이전 예제에 적용하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#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으로 바구면서 블로킹 상태에서 빠져나간다.

1
2
3
4
5
sem_wait(&sem);
/*
임계 영역
*/
sem_post(&sem);

아래의 코드는 “쓰레드 A가 프로그램 사용자로부터 값을 입력 받아서 전역변수 num에 저장하면, 쓰레드 B는 이 값을 가져다가 누적해 나간다. 이 과정은 총 5회 진행이 되고, 진행이 완료되면 총 누적 금액을 출력하면서 프로그램이 종료한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//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이외의 값 반환을 사용하는 것이 더 낫다.

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

Leave a comment