반응형
728x90
반응형

오늘은 C++에서의 쓰레드이다

지겹게 기억을 더듬어가면서 C기반의 포스팅을 했던 적이 있는데 C++에서도 지원을 한다니 새로웠다.

 

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

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

typingdog.tistory.com

위 링크는 쓰레드

C++ 에서의 쓰레드

원래 쓰레드의 생성과 사용은 OS에 종속적이었고, API가 C 기반이었다. 하지만 C++ 11 표준에 쓰레드 라이브러리가 따로 들어가면서 OS에 독립적이고, 프로그래밍 언어 차원에서 지원이 된다.

임계 영역과 관련된 문제는 Mutex를 통해서 상호배제를 처리할 것인데 이 또한 C++ 프로그래밍 언어 차원에서 지원이 된다는 것이다. 그래서 C 기반의 코드를 그대로 C++ 코드로 변경하도록 해보겠다. 

일단 간단하게 쓰레드 사용과 그에 동반하는 문제까지 설명하자면,

공용 데이터 영역의 변수(전역 변수)

쓰레드에서 공통으로 접근할 수 있는 데이터 영역의 변수를 두 쓰레드에서 임계영역에서 사용이 되면서 기대하는 값과 다른 결과가 나오는 것에서부터 문제가 된다.

쓰레드 A와 쓰레드 B에 해당하며 동일한 횟수만큼(백만) 증감

쓰레드 A에서는 동일한 횟수만큼 (1,000,000) 공용 Data 영역 변수를 1씩 증가시키고, 쓰레드 B에서는 동일한 횟수만큼 (1,000,000) 공용 Data 영역 변수를 1씩 감소시킨다.

기대하는 값은 0 이겠지만(동일한 횟수만큼 1씩 증가시키고, 감소시켰기 때문) 쓰레드 동기화가 이루어지지 않은 탓에 0이 아닌 다른 값이 나온다.

문제에 대한 자세한 설명은 아래의 링크를 통해서 확인하면 된다.

 

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

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

typingdog.tistory.com

C언어 기반 쓰레드 구현(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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
pthread_mutex_t mutex;
int common_value = 0;
 
void* t_main_plus(void *arg);
void* t_main_minus(void *arg);
 
int main(void)
{
    pthread_t tid1, tid2;
 
    pthread_mutex_init(&mutex, NULL);
 
    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); // 종료되지 않도록 대기.
    pthread_mutex_destroy(&mutex);
    printf("메인함수가 종료됩니다. [common_value의 최종 값 : %d]\n",common_value);
    return 0;
}
 
void* t_main_plus(void *arg)
{
    int i = 0;
 
    printf("t_main_plus 쓰레드가 연산을 시작합니다. \n");
 
    pthread_mutex_lock(&mutex);
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value+=1;
    pthread_mutex_unlock(&mutex);
 
    printf("t_main_plus 쓰레드가 종료됩니다.\n");
    return NULL;
}
 
 
void* t_main_minus(void *arg)
{
    int i = 0;
 
    printf("t_main_minus 쓰레드가 연산을 시작합니다. \n");
 
    pthread_mutex_lock(&mutex);
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value-=1;
    pthread_mutex_unlock(&mutex);
 
    printf("t_main_minus 쓰레드가 종료됩니다.\n");
    return NULL;
}
cs

C++언어 기반 쓰레드 구현(Mutex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <thread>
#include <mutex>
 
std::mutex mtx;
int comm_value = 0;
 
int main(void)
{
    std::thread t1([]() { mtx.lock(); for (int i = 0; i < 1000000; i++)    comm_value--; mtx.unlock(); });
    std::thread t2([]() { mtx.lock();  for (int i = 0; i < 1000000; i++)    comm_value++; mtx.unlock(); });
 
    t1.join();
    t2.join();
 
    std::cout << "comm_value의 값은 : " << comm_value << std::endl;
    return 0;
}
cs

 

위의 코드가 아래의 코드로 변환되었다고 생각하면 된다. 너무 간단하게 몇 줄이면 끝났다ㅋㅋㅋㅋ

끝.

728x90
반응형
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 - 14. 쓰레드의 치명적인 문제점

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

typingdog.tistory.com

이 포스팅에서는 멀티 쓰레드 기반병렬 처리에서 발생하는 임계 영역과 관련된 문제를 해결하기 위한 방법이다. 위는 그 문제점에 대해서 다루었던 포스팅이다.

멀티 쓰레드 기반의 병렬 처리에서 임계 영역과 관련된 문제를 해결하기 위한 방법으로는

뮤텍스
세마포어

등이 존재한다. 이 방법들은 쓰레드 동기화라고 하는데, 쓰레드의 접근 순서로 인해 발생하는 문제를 해결하기 위한 순서의 동기화를 의미한다. 

이름부터 무슨 세포 이름 같기도 하고, 이상한 인상을 받았다. 그러나 잠시 후에는 진짜 개쩌는 놈들이라는 것을 알게 될 것이다. 작성자는 깔끔하게 문제가 해결되는 것을 보고 감탄을 했었다ㅋㅋㅋㅋ ㄹㅇ

Mutex, 뮤텍스

먼저, 뮤텍스이다. Mutual Exclusion의 줄임말로.

구글 번역기

역시나 쓰레드끼리 공유하는 변수에 동시 접근을 막는 의미로 사용된다. 이걸 정말 어떻게 기록해야할까 고민을 많이 했는데 코드와 함께 비유를 섞어서 하는게 났겠다 싶다.

뮤텍스는 임계 영역을 대상으로 문을 하나 포함한 벽과, 열쇠를 만드는 것으로 비유하면 될 것이다.

이 벽으로 둘러 쌓인 임계 영역에는 딱 한 사람만(하나의 쓰레드 실행 흐름만) 들어갈 수 있으며, 들어갈 때는 꼭 열쇠로 문을 잠그고 나올 때는 열쇠로 문을 열어 놓고 나간다.

근데 이상한 점이 있다. 그림으로만 보면 임계영역이라는 것은 하나인데, t_main_plus 함수와 t_main_minus 함수내에서 서로 다른 코드 영역이다. 

좀 더 정확히 말하자면 벽과 열쇠를 가져다가 사용할 수 있는 권한을 lock과 unlock을 통해 얻고 문을 열고 닫는 것이라고 생각하면 될 것 같다. "쓰레드가 각자 임계 영역에서 계산 및 작업을 할 것인데 벽과 열쇠로 둘러쌓지 않으면 작업을 못한다"라고 규정해버리면 자연스럽게 대기를 할 것이다. 

그런데 서로 다른 라인의 코드 영역이긴 하지만, 결국에는 접근하여 조작하는 변수는 같기 때문에 그림으로는 하나의 영역에 들어가는 것처럼 표현한 것이다. 조금 유연하게 이해를 해야할 필요가 있는 것 같다.. 아니면 조금 더 코드의 레벨로..? 

아무튼 그래서 뮤텍스 관련 함수를 한 번에 기록하겠다. 별로 필요로 하는 인자가 없기 때문에 간단하게 정리한다.

pthread_mutex_init, pthread_mutex_destroy, pthread_mutex_lock, pthread_mutex_unlock

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

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
pthread_mutex_t mutex;
int common_value = 0;
 
void* t_main_plus(void *arg);
void* t_main_minus(void *arg);
 
int main(void)
{
    pthread_t tid1, tid2;
 
    pthread_mutex_init(&mutex, NULL);
 
    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); // 종료되지 않도록 대기.
    pthread_mutex_destroy(&mutex);
    printf("메인함수가 종료됩니다. [common_value의 최종 값 : %d]\n",common_value);
    return 0;
}
 
void* t_main_plus(void *arg)
{
    int i = 0;
 
    printf("t_main_plus 쓰레드가 연산을 시작합니다. \n");
 
    pthread_mutex_lock(&mutex);
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value+=1;
    pthread_mutex_unlock(&mutex);
 
    printf("t_main_plus 쓰레드가 종료됩니다.\n");
    return NULL;
}
 
 
void* t_main_minus(void *arg)
{
    int i = 0;
 
    printf("t_main_minus 쓰레드가 연산을 시작합니다. \n");
 
    pthread_mutex_lock(&mutex);
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value-=1;
    pthread_mutex_unlock(&mutex);
 
    printf("t_main_minus 쓰레드가 종료됩니다.\n");
    return NULL;
}
cs

몇 번을 실행해도 백만씩 더하고, 빼기가 정확하게 이루어져 0을 이루는 모습이다. ㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷ

교착 상태, 데드락

이렇게 lock / unlock 기법을 사용하는 곳에 꼭 동반하는 문제가 무어냐면 교착 상태라는 데드락 상태이다.

그림의 숫자 순서대로 읽으면 되고, 이를 꼭 주의해야 한다. 1번의 경우는 생각보다 잘 나올 수 있다. lock을 건 쓰레드가 뻑이나거나 사망해버릴 경우 문이 잠긴 채로 unlock이 호출되지 않고 교착 상태에 들어가는데 이런 경우는 답이 없다 ㅋㅋㅋㅋ 디비에서도 그렇고 운영체제에서도 그렇고 비슷한 일들이 일어난다.

다음은 세마포어이다 (이진값을 이용한 세마포어)

728x90
반응형
728x90
반응형
 

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

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

typingdog.tistory.com

 

 

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

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

typingdog.tistory.com

쓰레드 이전 설명들이다. ㅋㅋ

이렇게 프로세스의 자식 프로세스의 반환 회수, 프로세스끼리 통신의 까다로움, 많은 메모리 자원 소모, 부담스러운 컨텍스트 스위칭의 오버 헤드 문제들이 거의 해결이 되는 전지전능한 쓰레드일 것이라고 생각했다.

그러나 언제나 그렇듯, 하나가 되면 하나가 문제가 생기는게 이치이듯이 쓰레드 또한 치명적인 문제가 있었다.

통신을 손쉽게 하기 위해서 힙이나 데이터 등의 공유 메모리 영역을 두었다. 아주 잘한 것이다. 그런데 이 공유 메모리 영역에서 여러 다른 기능을 하는 쓰레드들이 동일한 변수 등에 접근하여 CRUD가 일어나는 경우 문제가 생기는 것이다! 왜냐하면 다음과 같은 상황인 것이다.

후 그리느라 너무 힘들었다. 위와 같은 상황을 이야기하는 것이다. 순서대로 읽으면 된다. 그림에서 나타내는 바를 설명하자면 같은 메모리 영역을 동시에 혹은 순서의 정함이 없이 읽고 쓰는 경우, 해당 메모리 영역의 값에 대한 기대값을 보장할 수 없게 된다. 왜 그럴까? 이 또한 그림으로 준비해보았다. 

위의 순서대로 읽으면 된다.

결국, 쓰레드를 통한 병렬 처리가 진행될 때에는 프로세스와는 달리 공통적인 메모리 영역이 존재한다. 그래서 여기서 선언된 공통적인 변수에 동일한 임계 영역을 갖는 쓰레드들이 순서의 정함이 없거나, 동시에 접근을 하는 경우, 해당 변수로부터의 기대값을 보장할 수 없는 것이다. 다음 예를 통해서 확인해보겠다. 

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
int common_value = 0;
 
void* t_main_plus(void *arg);
void* t_main_minus(void *arg);
 
int main(void)
{
    pthread_t tid1, tid2;
 
    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); // 종료되지 않도록 대기.
    printf("메인함수가 종료됩니다. [common_value의 최종 값 : %d]\n",common_value);
    return 0;
}
 
void* t_main_plus(void *arg)
{
    int i = 0;
 
    printf("t_main_plus 쓰레드가 연산을 시작합니다. \n");
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value+=1;
 
    printf("t_main_plus 쓰레드가 종료됩니다.\n");
    return NULL;
}
 
 
void* t_main_minus(void *arg)
{
    int i = 0;
 
    printf("t_main_minus 쓰레드가 연산을 시작합니다. \n");
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value-=1;
 
    printf("t_main_minus 쓰레드가 종료됩니다.\n");
    return NULL;
}
cs

서로 다른 연산을 하는 두 t_main_plus, t_main_minus 함수가 존재한다. 그리고 이 함수들은 전역 변수인 common_value 변수의 값을 +1, -1 조작하는 임계 영역을 가지고 있다.

그래서 각각 백만 번씩 1을 더하거나 빼도록 쓰레드를 동시에 실행하고 있는 상황이다.

t_main_plus에서는 백만번을 더하고, t_main_minus에서는 백만번을 빼니까 결국 둘이 상쇄되서 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
반응형

+ Recent posts