반응형
728x90
반응형
 

C기반 I/O Multithreading - 12. 멀티 프로세싱? 멀티 쓰레딩?

멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거리도 만만치 않기 때문에 좀 상세히 볼 필요가 있다ㅋ

typingdog.tistory.com

 

 

C기반 I/O Multithreading - 13. 쓰레드의 생성과 소멸까지

C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거

typingdog.tistory.com

 

 

C기반 I/O Multithreading - 14. 쓰레드의 치명적인 문제점

C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거

typingdog.tistory.com

 

 

C기반 I/O Multithreading - 15. 뮤텍스와 세마포어(1)

C기반 I/O Multithreading - 14. 쓰레드의 치명적인 문제점 C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사

typingdog.tistory.com

 

 

C기반 I/O Multithreading - 16. 뮤텍스와 세마포어(2)

C기반 I/O Multithreading - 15. 뮤텍스와 세마포어(1) C기반 I/O Multithreading - 14. 쓰레드의 치명적인 문제점 C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프..

typingdog.tistory.com

멀티 스레드와 이전 포스팅 시리즈(?)들이다.

이번에는 멀티 스레드를 기반으로 하며 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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
 
#include <pthread.h>
 
#define BUFSIZE 30
#define MAXCLIENTS 30
 
pthread_mutex_t mutex;
int client[MAXCLIENTS] = { 0, };
int now = 0;
 
void * HandleClient(void * arg);
void ErrorMsg(const char *msg);
 
int main(void)
{
    pthread_t tid;
 
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
 
    socklen_t addr_size;
 
 
    pthread_mutex_init(&mutex, NULL); // 뮤텍스 id 생성
 
    serv_sock = socket(PF_INET,SOCK_STREAM,0);
    if(serv_sock == -1)
        ErrorMsg("socket() error");
 
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(7010);
 
    if(bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        ErrorMsg("bind() error");
 
    if(listen(serv_sock, 5== -1)
        ErrorMsg("listen() error");
 
    while(1)
    {
        addr_size = sizeof(clnt_addr);
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &addr_size);
        if(clnt_sock == -1)
            continue;
 
 
        pthread_mutex_lock(&mutex);
        client[now++= clnt_sock;
        pthread_mutex_unlock(&mutex);
 
        pthread_create(&tid, NULL, HandleClient, (void*)&clnt_sock);
        pthread_detach(tid);
 
        printf("클라이언트 연결 [%d]\n", clnt_sock);
    }
    return 0;
}
 
void * HandleClient(void * arg)
{
    int clnt_sock = *((int*)arg);
    int strLen, state;
    char buf[BUFSIZE];
    int i = 0;
 
    while((strLen=read(clnt_sock, buf, sizeof(buf)))!=0)
    {
        pthread_mutex_lock(&mutex);
        for (i = 0; i < now; i++)
            write(client[i], buf, strLen);
        pthread_mutex_unlock(&mutex);
    }
 
    pthread_mutex_lock(&mutex);
    for (i = 0; i < now; i++)
    {
        if(clnt_sock == client[i])
        {
            printf("클라이언트 연결 종료 [%d]\n", client[i]);
            for(;i<now-1;i++)
                client[i] = client[i+1];
            break;
        }
    }
    now--;
    pthread_mutex_unlock(&mutex);
    close(clnt_sock);
    return NULL;
}
 
void ErrorMsg(const char *msg)
{
    fputs(msg, stderr);
    fputc('\n',stderr);
    exit(1);
}
 
cs

문제 없이 통신이 잘 되는 모습이다. 접속한 클라이언트의 수가 너무 적어서 사실 확실히 확인한 케이스는 절대 아니지만 코드적으로만 보아도 의미가 있기 때문에 이것으로 마무리 짓겠다.

728x90
반응형
728x90
반응형
 

C기반 I/O Multithreading - 15. 뮤텍스와 세마포어(1)

C기반 I/O Multithreading - 14. 쓰레드의 치명적인 문제점 C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사

typingdog.tistory.com

쓰레드를 동기화 하는 방법으로 뮤텍스에 이어서 세마포어다! 

좀 더 구체적으로 정확히 이야기하자면 바이너리 세마포어이다.

바이너리 세마포어란

이 바이너리 세마포어는 0과 1을 사용하여 실행 순서를 컨트롤 한다. 다음은 바이너리 세마포어가 임계영역에 대한 접근 순서를 동기화하는 과정을 적어놨다.

쓰레드 A가 먼저 sem_wait() 함수를 호출하여 임계 영역에 접근한 상태에서 쓰레드 B가 sem_wait() 함수를 호출하여 블로킹에 걸렸다가, 쓰레드 A가 sem_post() 함수를 호출하여 쓰레드 B가 sem_wait() 함수 처리 가능 상태가 되어서 이를 호출하고, 쓰레드 B가 임계 영역에 접근한 뒤, sem_post() 함수를 호출하여 쓰레드 B가 임계 영역에서 빠져나오는 과정을 설명한 것이다.

좀 설명이 복잡했지만 다음과 같다.

위에서 나온 함수들의 원형을 살펴보면 다음과 같다.

sem_init, sem_destroy, sem_wait, sem_post

이어서 간단한 예제를 확인하겠다. 지난 예제의 보완이다.

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 <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
 
sem_t semaphore;
int common_value = 0;
 
void* t_main_plus(void *arg);
void* t_main_minus(void *arg);
 
int main(void)
{
    pthread_t tid1, tid2;
 
    sem_init(&semaphore, 01);
    // 세 번째 인자를 0으로 넣으면 sem_wait을 먼저 호출한 쓰레드에서 블로킹이 발생하는 것을 확인할 수 있다.
 
    pthread_create(&tid1, NULL, t_main_plus, NULL); // 쓰레드 생성
    pthread_create(&tid2, NULL, t_main_minus, NULL); // 쓰레드 생성
 
    pthread_detach(tid1); // tid1 에 해당하는 쓰레드가 종료됨과 동시에 소멸.
    pthread_detach(tid2); // tid2 에 해당하는 쓰레드가 종료됨과 동시에 소멸.
 
    sleep(7); // 종료되지 않도록 대기.
    sem_destroy(&semaphore);
 
    printf("메인함수가 종료됩니다. [common_value의 최종 값 : %d]\n",common_value);
    return 0;
}
 
void* t_main_plus(void *arg)
{
    int i = 0;
 
    printf("t_main_plus 쓰레드가 연산을 시작합니다. \n");
 
    sem_wait(&semaphore);
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value+=1;
    sem_post(&semaphore);
 
    printf("t_main_plus 쓰레드가 종료됩니다.\n");
    return NULL;
}
 
 
void* t_main_minus(void *arg)
{
    int i = 0;
 
    printf("t_main_minus 쓰레드가 연산을 시작합니다. \n");
 
    sem_wait(&semaphore);
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value-=1;
    sem_post(&semaphore);
 
    printf("t_main_minus 쓰레드가 종료됩니다.\n");
    return NULL;
}
cs

여러 차례 실행해도 common_value 값은 딱 0으로 맞아 떨어진다. 세마포어가 교착 상태도 없이 잘 수행해낸 것으로 확인되는 실행 결과이다. 

이로서 쓰레드 동기화에 관련된 내용은 마무리이고 다음 포스팅에는 뮤텍스를 이용한 서버 프로그램 작성이다.

728x90
반응형
728x90
반응형
 

C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩?

멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거리도 만만치 않기 때문에 좀 상세히 볼 필요가 있다ㅋ

typingdog.tistory.com

위의 포스팅에 이어서 작성하는 글이다.

코드를 통해 쓰레드를 생성부터 소멸까지 한번 확인해 볼 것이다.

쓰레드의 생성

쓰레드의 생성하는 함수에 대한 설명이며, 위 함수를 통해 쓰레드를 생성하는 예제를 실행해볼 것이다.

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 <unistd.h>
#include <pthread.h>
 
void* t_main(void *arg);
 
int main(void)
{
    pthread_t tid;
    int t_param = 8;
 
    if(pthread_create(&tid, NULL, t_main, (void *)&t_param)!=0// 3,4번째의 인자들의 자료형을 잘 맞춰주어야 한다.
    {
        printf("반환 값이 0이 아닌 값으로 pthread_create에 실패했습니다.\n");
        return -1;
    }
    sleep(10);
    printf("메인함수가 종료됩니다.\n");
 
    return 0;
}
 
void* t_main(void *arg)
{
    int i;
    int cnt = *((int*)arg);
 
    for(i=0; i<cnt; i++)
    {
        printf("thread_main이 실행중입니다\n");
        sleep(1);
    }
    return NULL;
}
cs

위의 예제를 실행한 결과이다. 메인 함수가 바로 종료되면 프로세스가 종료되기 때문에 그 내부의 실행 흐름인 쓰레드 또한 함께 바로 종료되기 때문에 메인 함수를 종료시키지 않고 붙잡아 둘 필요가 있었던 것이다. 그래서 17번 라인에서 sleep 함수로 10초를 버틴다!

위의 예제에 대한 실행 흐름을 그림으로 나타낸 것이다.

그런데 문제가 있다! 이전에 프로세스 이야기에서도 마찬가지였지만 sleep 함수를 통해서 프로그램의 실행 흐름을 대충 맞춘다는게 이게 말이나 되는 상황인가? 이러한 문제를 해결하기 위한 함수가 존재한다.

쓰레드가 종료할 때까지 대기! : pthread_join 함수의 등장 

첫 번째 인자로 넘어간 tid 값에 해당하는 쓰레드가 종료될 때 까지 main 함수는 대기를 한다!

코드를 보고 확인하겠다.

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
void* t_main(void *arg);
 
int main(void)
{
    pthread_t tid;
    int t_param = 8;
    int * return_val = NULL;
 
    if(pthread_create(&tid, NULL, t_main, (void *)&t_param) != 0// 3,4번째의 인자들의 자료형을 잘 맞춰주어야 한다.
    {
        printf("반환 값이 0이 아닌 값으로 pthread_create에 실패했습니다.\n");
        return -1;
    }
 
    if(pthread_join(tid, (void **)&return_val) != 0)
    {
        printf("반환 값이 0이 아닌 값으로 pthread_join에 실패했습니다.\n");
        return -1;
    }
    printf("pthread_join 함수 호출 직 후\n");
    printf("쓰레드 메인 함수에서 반환된 값 : [%d] \n"*return_val);
    printf("메인함수가 종료됩니다.\n");
    free(return_val);
    return 0;
}
 
void* t_main(void *arg)
{
    int i;
    int cnt = *((int*)arg);
    int * thread_return_value = (int *)malloc(sizeof(int));
 
    printf("thread_main이 실행 되었습니다!\n");
    printf("반환 값 계산 후, 잠시 10초 대기하겠습니다ㅋㅋ - main에서 pthread_join의 효과를 보기 위함.\n");
 
    *thread_return_value = 100;
    *thread_return_value = (*thread_return_value) * cnt;
 
    sleep(10);
    return thread_return_value;
}
cs

중간 중간에 설명해야할 부분이 있는 것 같다 ㅋㅋ 포인터들이 좀 많이 깔려있는데 다른 예제들에 비해서. 근데 전혀. 어렵지 않다. 

위 코드에서는 그림에 해당하는 부분으로 설명이 될 것 같다.

다만, 이렇게 더블 포인터까지 써가면서 가리키는 이유는 쓰레드 간의 통신이기 때문에 공유 메모리 영역인 힙 영역의 메모리 공간에 값을 저장하고 또 그것을 외부에서 참조해서 쓰는 것이다. 아래는 실행 결과이다.

아래는 실행 흐름을 그림으로 나타낸 것이다.

 

쓰레드의 소멸

쓰레드는 꼭 직접 소멸을 해줘야한다! 쓰레드의 main 함수(위에서는 t_main)를 반환했다고 자동 소멸이 되는 것이 절대 아니다.

그러면 뭐 closesocket 같은 전용 close 형식의 함수가 존재하는가? 그렇다! 존재한다! 다만 close~ 형태는 아니고 이미 본 것도 존재한다. 바로 아래의 함수들이다.

첫 번째는 이미 봤던 pthread_join 함수이다. 이 함수를 호출하면 해당 tid의 쓰레드가 종료될 때까지 대기할뿐만 아니라 소멸까지도 책임지고 담당한다. 그러나 치명적인 단점은 말 그대로 main 함수를 대기시키는 블로킹에 빠진다는 점.

그래서 나온 것이 다음이다.

이 함수를 사용하면 쓰레드의 소멸까지 담당하여 처리되는데, 중요한 것은 블로킹에 빠지지 않는다는 것이다!

다음 예제를 통해서 보겠다.

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
void* t_main(void *arg);
 
int main(void)
{
    pthread_t tid;
    int t_param = 8;
 
    int i = 0;
 
    if(pthread_create(&tid, NULL, t_main, (void *)&t_param) != 0// 3,4번째의 인자들의 자료형을 잘 맞춰주어야 한다.
    {
        printf("반환 값이 0이 아닌 값으로 pthread_create에 실패했습니다.\n");
        return -1;
    }
 
    printf("pthread_detach 함수 호출!\n");
    pthread_detach(tid); // tid 에 해당하는 쓰레드가 종료됨과 동시에 소멸시킨다.
 
    for(i=0; i<7; i++)
    {
        sleep(1);
        printf("쓰레드가 이미 종료되었지만, 블로킹 되지 않고 main만의 작업을 진행하다가 \n");
    }
    printf("main만의 작업 루틴 종료.\n");
    printf("메인함수가 종료됩니다.\n");
    return 0;
}
 
void* t_main(void *arg)
{
    int cnt = *((int*)arg);
 
    printf("thread_main이 실행 되었습니다!\n");
    printf("받은 값은 [%d]\n", cnt);
 
    printf("t_main 리턴 직전(종료라고 보면 됨)\n");
    return NULL;
}
cs

아래는 실행 결과로 블로킹이 없음을 확인할 수 있다.

위 예제에 대한 실행 흐름을 그림으로 나타내보았다.

 

쓰레드의 개념부터 생성 및 소멸까지 확인해보았다.

앞으로 남은 포스팅은 임계 영역에 대한 설명과 서버 프로그래밍이다. ㅋㅋ 제일 고비이다 고비..

728x90
반응형
728x90
반응형

멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거리도 만만치 않기 때문에 좀 상세히 볼 필요가 있다ㅋㅋ

아래는 멀티 프로세싱에 대한 포스팅의 시작이고, 그 아래에 있는 멀티 프로세싱 포스팅 중 제일 마지막 시리즈(?)에 프로세스 생성과 복사에 대한 개념이 조금 들어간다.

 

C기반 I/O Multiprocessing - 8. 드디어 올리는 멀티 프로세싱

진짜 다 까먹어서 다시 예제 쳐보고 다시 올린다... 정말 미리미리 올리고 자주자주 봐야합니다.. 포스팅 순서가 사실 멀티 프로세싱이 먼저이지만, 게임 서버 개발에 사용한 것은 멀티 플렉싱

typingdog.tistory.com

 

 

C기반 I/O Multiprocessing - 11. 프로세스 정리 및 IPC

C기반 I/O Multiprocessing - 8. 드디어 올리는 멀티 프로세싱 진짜 다 까먹어서 다시 예제 쳐보고 다시 올린다... 정말 미리미리 올리고 자주자주 봐야합니다.. 포스팅 순서가 사실 멀티 프로세싱이 먼

typingdog.tistory.com

링크 올리면서 발견한건데.. 예제 쳐보고가 아니라 예제를 '타이핑'해보고...ㅋㅋㅋㅋ

멀티 프로세스의 특징과 단점

자, 일단은 멀티 프로세싱의 단점을 확인할 필요가 있다. 왜냐하면 멀티 쓰레딩멀티 프로세싱과 유사한 기능을 제공하지만 단점을 보완할 수 있는 개념이기 때문이다. 멀티 프로세스의 장점은 멀티 쓰레딩의 단점을 통해 확인해야하기 때문에 지금 이 포스팅에서는 언급하지 않겠다.

멀티 쓰레드는 겉에서 봤을 때에는 멀티 프로세스와 비스므리한 개념이다.
멀티 프로세스의 특징을 간단하게 다시 살펴보자.

-멀티 프로세스의 특징

1. 프로세스의 복사는 사실 프로세스 생성을 거친다.
2. 프로세스의 복사는 스택, 데이터, 힙 영역이 모두 복사되고, 부모와 자식은 서로 독립적인 메모리 공간을 갖는다.
3. 프로세스의 복사는 파일 디스크립터 복사로 인해 파일 디스크립터와 소켓의 연결 관계를 n:1로 만든다
4. 프로세스의 복사는 부모 프로세스와 자식 프로세스로 나뉘며, 부모의 프로세스가 자식의 프로세스 반환을 꼭 회수해야한다.

말 그대로 복사이기 때문에 위와 같은 현상들이 일어나는데, 이러한 특징들로 인해서 멀티 프로세스의 단점은 다음과 같다. 위의 멀티 프로세스의 특징에 대응되도록 색을 칠했다.

-멀티 프로세스의 단점

1. 프로세스 복사 과정은 시스템에 굉장히 부담스럽다. (프로세스의 생성 자체가 부담스럽다)
2. 프로세스는 서로 별개의 요소이기 때문에 별개의 독립된 메모리 공간을 갖고, 이로 인해서 프로세스 간에 통신이 어렵다 ( 사실 이건 장점일수도?ㅋㅋ )
3. 파일 디스크립터와 소켓의 연결 관계가 n:1일 경우, 소켓에 해당하는 모든 파일 디스크립터를 종료해야 비로소 소켓이 종료된다.
4. 프로세스의 복사는 부모 프로세스와 자식 프로세스로 나뉘는데, 이로 인해서 자식 프로세스의 반환 값 회수가 이루어지지 않으면 좀비 프로세스가 발생한다.
5. CPU가 프로세스 별로 시간을 할당해서 여러 프로세스를 처리하는 시분할 처리를 하기 위해 현재 처리할 프로세스를 메모리에 올리고, 현재 처리하지 않는 프로세스를 메모리에 내리는 작업을 PCB를 이용하여 행하는데 이를 컨텍스트 스위칭이라는 과정이다. 이러한 과정은 하드 디스크 레벨의 I/O 까지 연계되기 때문에 부담이 굉장히 크다.

-멀티 쓰레드는 멀티 프로세스의 어떤 부분들이 어떻게 커버되었을까?

단점이 생각보다 많다ㅋㅋㅋㅋㅋ 멀티 쓰레드는 멀티 프로세스의 단점을 커버하기 위해서 등장했다. ( 물론 전적으로 그렇지는 않겠다만ㅋ... ) 마찬가지로 위의 단점들에 대응되도록 색을 칠했다.

1. 쓰레드는 복사하는 과정이 아니며, 쓰레드의 생성과 쓰레드의 컨텍스트 스위칭은 프로세스의 그것들보다 빠르다.
2. 쓰레드는 메모리 영역을 통째로 복사하지 않고, 일부를 공유하며(힙, 데이터), 병렬 흐름에 필요한 부분(스택)만 독립적으로 갖는다. 그렇기 때문에 공유되는 영역을 통해 쓰레드 간의 통신이 가능하다.
3. 쓰레드의 생성은 프로세스처럼 통째로 복사하는 과정이 아니기 때문에 해당하지 않는다. 프로세스에는 File Descriptor table을 갖는데, 프로세스가 복사되면 이 테이블 자체가 복사된다. 그렇기 때문에 문제가 되는 것이고, 파일 디스크립터의 복사는 단순히 파일 디스크립터 값을 다른 변수로 넘겨주네 마네 하는 차원이 아니라는 소리다.
4. 프로세스처럼 통째로 복사되는 과정이 아니기 때문에 해당하지 않는다.
5. 쓰레드의 컨텍스트 스위칭은 쓰레드 라이브러리에 의해 TCB(Thread Controll Block) 을 참조하여 발생하며, OS 스케줄러에 의해 PCB를 참조하여 컨텍스트 스위칭되는 프로세스의 과정보다는 훨씬 빠르다.

-이런 멀티 프로세스의 단점을 커버하는 멀티 쓰레드(쓰레드)는 무엇인가?

위는 프로세스에 대한 포스팅에서 간단하게 정리한 정보인데, 프로세스와 비교하면서 보면 더 와닿을 것 같아서 다시 올렸다. 

프로세스 관점에서 별도의 실행 흐름을 구성하는 단위.
생성될 때, 프로세스와 마찬가지로 TCB(Thread Controll Block) 라고 하는 쓰레드에 대한 정보 구조가 생성된다.

쓰레드는 프로세스의 일부이며 실행의 단위이기 때문에 TCB에는 PCB만큼의 많은 정보가 저장되지는 않는다.
또한, 프로세스의 일부이기 때문에 프로세스가 할당 받은 메모리의 공간을 공유할 수 있다. (통신에 용이)

-그렇다면 멀티 쓰레드란 무엇일까?

히..힘들었..다..

프로세스 내에 여러 쓰레드를 운용하여 병렬 처리하는 방법을 멀티 쓰레드라고 한다.

개념적인 부분은 이 정도로 마무리 하고 다음 포스팅에서는 쓰레드 생성 및 소멸 등의 과정을 코드로 볼 것이다.

728x90
반응형
728x90
반응형
 

C기반 I/O Multiplexing - 2. Select 함수 사용하기 전에 컨셉을 보자

C기반 I/O Multiplexing - 1. Multiprocessing과 Multiplexing 비유 최근, 아니 비교적 최근에 어떤 게임 클라이언트를 활용한 게임 서버를 만드는 프로젝트를 진행했던 적이 있고, 지금도 조금씩 조금씩 하고

typingdog.tistory.com

여기서 개념과 컨셉을 확인했다.

코드 분석도 중요하다. 왜냐하면 코드에서 드러나는 select 기반 모델의 특징들이 있기 때문이다.

먼저, 필요로 하는 변수들이다.

코드는 중요한 변수만 넣었고, 그림에는 주석과 내가 작성한 그대로 캡춰한 것이니 함께 보면 좋을 것 같다.

struct timeval timeout; // select 함수의 시간을 지정.
fd_set fd_pool; // 비트 배열이라고 생각.

 

다음으로는 초기화 과정이다

FD_ZERO(&this->fd_pool);
FD_SET(this->serv_sock, &this->fd_pool); 
 // this->serv_sock = socket(PF_INET, SOCK_STREAM, 0); 에서의 리턴 값이다. 
this->fd_max = this->serv_sock; //초기화 하는 시점을 기준으로 가장 나중에 생성된 파일 디스크립터.

 

이어서, 접속 요청이나 데이터 수신을 위해 select 함수를 호출하는 부분이다

// select 함수 호출 시, 값이 리셋되므로 this->fd_pool을 바로 사용하지 않고 임시 변수 fd_temp_pool로 사용한다.
fd_set fd_temp_pool = this->fd_pool; 

// 타임 설정
this->timeout.tv_sec = select_sec;
this->timeout.tv_usec = select_usec;

this->fd_num = select(this->fd_max + 1, &fd_temp_pool, 0, 0, &this->timeout);
// fd_max+1인 이유는 파일 디스크립터는 0부터 시작하며 생성될 때마다 1씩 증가한다. 
// 그러므로 생성된 파일 디스크립터 중 최대값에 +1을 하면 그 갯수가 나온다.

fd_temp_pool에 대입하는 부분이 중요하다. select 함수가 호출하면 변화되는 부분을 표현할 때, 인자로 넘어온 fd_set 주소 값을 이용하여 변화된 파일 디스크립터 외에 나머지에 해당하는 부분은 값을 탈락시킨다.

그렇기 때문에 등록해놓은 파일 디스크립터 정보들이 탈락할 수 있는, 원본이 바뀌는 구조이기 때문에 꼭 임시 변수에 최신화된 파일 디스크립터 정보들을 넣어주고, 임시 변수를 통해서 select 함수로의 인자 전달이 이루어져야 한다.

다음은 select 함수 호출로 받아온 리턴 값별 처리이다.

--

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
if (this->fd_num > 0 ) // >0:성공,
    if (FD_ISSET(this->serv_sock, &fd_temp_pool)) 
    {    // fd_pool에 serv_sock에 대한 변화가 존재한다면 양수 반환 -> 서버 소켓의 변화 -> 접속 의미.
        AcceptConnectionRequest(this->serv_sock);
        this->PrintConsoleNowUser();
    }
    else // 기존 접속된 클라이언트의 수신 및 종료
    {
        int r_var;
        for (int i = 0; i < this->now_user_count; i++// 이렇게 순회를 도는 이유는 다음에 나오는 this->client[i] 때문에.
        {
            if (FD_ISSET(this->client[i], &(fd_temp_pool)))
            {
                r_var = this->ReceiveData(this->client[i]);
                if (r_var == Server::SERVER__RETURNVAR__DISCONNECT_USER) {
                    this->Disconnect(this->client[i]);
                    this->PrintConsoleNowUser();
                    break// 동시에 여러 디스크립터들이 변화할 수 있는가를 확인해 본후 주석
                 } else {
                    if (this->HandlePacketMessage(this->client[i]) == Server::SERVER__RETURNVAR__TERMINATE_BY_ADMIN)
                        return Server::SERVER__RETURNVAR__TERMINATE_BY_ADMIN;
                    break// 동시에 여러 디스크립터들이 변화할 수 있는가를 확인해 본후 주석
                }
            }
        }
    }
else if (this->fd_num == 0// 0:timeout,
    return 0;
else // -1:실패
    HandleStatus("SERVER"true"Select() Error\n");
cs

fd_num이라는 이름의 정수형 변수로 리턴 값을 받는다. 이 때 넘어온 리턴 값에 따라 처리를 하는데, 일단 fd_num이 0보다 크다는 소리는 등록된 파일 디스크립터들 중에서 수신 관련하여 변화가 그 수 만큼 있었다는 소리이다.

아니, 그러면! select 한 번 호출 뒤 반환 값이 5가 나오면 5개의 변화가 있었다는 소리이고, 그러면 다섯 번을 처리해줘야 하는데, 현재 조건문 구조로는 다섯 번을 절대 처리하지 않는다. 이게 도대체 무슨 이유일까? 아니 무슨 말을 하고 있는지 이해가 가는가?

위 그림과 같다. 현재 5개의 파일 디스크립터에 변화가 발생했다.( 1번의 접속 요청 변화 , 4번의 수신 요청 변화 )

2번 줄을 보면 serv_sock의 변화가 생겼다. serv_sock에 수신할 데이터가 있다는 소리고 listen 소켓에서 수신할 데이터가 있다는 소리는 접속 처리를 한다는 소리이다. 근데 2번과 7번이 if ~ else 구조이기 때문에 if문에서 한 번 처리해버리면 다른 처리는 그냥 넘어가게 된다. 무시된다는 소리다.

하지만 이는 select 모델의 레벨 트리거적인 특성으로 인해 처리되지 않은 이벤트가 다음 select 함수 호출 때, 다시 드러나게 되고 이를 처리하게 된다. 

아니 그러면 이벤트가 발생했을 때 다 처리할 수 있게끔 하면 되지 않는가? -- 이건 그냥 코드 작성 스타일이다. 이벤트가 전부 감지 되었을 때 다 처리를 하려거든 매 select 마다 많은 루프를 돌려야 한다. 아래와 같이 말이다.

for( i < fd_num ) // 모든 접속자 혹은 리스너 파일 디스크립터들에 대해서 
{
    if(FD_ISSET(i, fd_temp_pool)) // 변화가 있었는지 검사를 하고 다녀야 한다.
    { 
        ...
    }
}

 

뭐 아무튼 본론으로 돌아와서, 위 코드에서 2번 줄의 접속 요청 수신 관련 listen 소켓에서의 변화가 일어나지 않았다면 else 구문인 7번 줄에 들어가고 이 경우는 데이터 수신에 관련된 경우이다. 

그러니까 파일 디스크립터의 변화가 있긴 있었는데 (1번 줄) 접속 요청은 아니고(2번줄), 수신에 관련된 파일 디스크립터의 변화가 있었던 것이다. 이 경우에는 어떤 파일 디스크립터에서 변화가 생겼는지 알아보기 위해 순회한다. 12번 줄을 통해 처리한다.

그리고 27번 줄에서는 select 반환 값이 0인 경우로, select에 지정된 시간 동안 파일 디스크립터들에서 아무 변화가 없었던 케이스가 된다. 뭐 이 경우에는 다시 select 함수를 호출하면 된다.

좀 더 세분화 했을 때, 접속 요청에 따른 수락 시 클라이언트 파일 디스크립터의 등록이다.

좀 더 세분화 했을 때, 변화가 발생한 클라이언트 파일 디스크립터로 부터 수신처리이다

접속한 클라이언트가 종료할 경우를 확인한다

FD_CLR(disconnect_target_fd, &this->fd_pool);
close(disconnect_target_fd);
this->now_user_count--;

종료하려는 클라이언트의 파일 디스크립터를 당연히 원본 fd_set의 배열에서 빼줘야한다. 종료하는데 지니고 있어서 뭐 하는가 ㅎㅎ..

대충 큰 처리 방법들은 한 번씩 다 훑어봤다.

다음은 간단하게 Level Trigger 와 Edge Trigger에 대해서 정말 간단하게 정리해보고자한다.

728x90
반응형

+ Recent posts