반응형
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

 

 

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 Multiprocessing - 8. 드디어 올리는 멀티 프로세싱

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

typingdog.tistory.com

 

C기반 I/O Multiprocessing - 9. 멀티 프로세싱에서 좀비 문제

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

typingdog.tistory.com

 

C기반 I/O Multiprocessing - 10. 멀티 프로세싱 기반 서버

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

typingdog.tistory.com

위 링크들에서 볼 수 있듯이 멀티 프로세싱의 주요 맥락을 확인하고, 포스팅하여 기록해보았다. 그런데 말입니다, 부모 프로세스와 자식 프로세스들 간에 통신이 필요한 경우는 어떻게 해야할까?

아니, 사실 프로세스들 간에 통신을 안 하는게 최고라고 나는 생각하지만 또, 그게 아닐 수 있으니 프로세스들 간의 통신 방법에 대해서 공부하고 넘어가는게 났겠다 싶어서 전에 공부를 해 보았다. 

일단, 프로세스들 간의 통신에 대해서 정리하기 전에 멀티 프로세스에 대한 정리를 한번 할 필요가 있다. 왜냐하면 이전 포스팅에서는 코드 구현 관점에서만 설명이 되어서 프로세스에 대한 충분한 설명이 이루어지지 않았다.

멀티 프로세스 개념 간단 정리

살짝 마음에 안들지만..

1. fork 함수를 통해서 프로세스가 복사되는데, 이 때, 복사라는 과정은 사실 새로운 프로세스의 생성 과정을 거친다.( PCB, Process Controll Block 이 함께 생성되어 생성된 프로세스에 대한 정보들을 저장하는 자료구조 블록이며, 컨텍스트 스위칭 때 이 정보를 활용한다)

2. fork 함수를 통해서 프로세스가 복사 시에는 부모 프로세스의 스택, 데이터, 힙 영역이 모두 복사되어 부모와 자식은 완전히 별개의 메모리 공간을 갖으며 내용 또한 그대로 복사된다.

자, 1번과 2번의 특징으로 인해서 프로세스 간에는 아무리 부모와 자식 사이의 관계라고 하더라도 서로 독립적인 메모리 공간을 가지고 있기 때문에 통신하기가 힘들다. 그래서 프로세스 간의 대화 방법으로 나온 것이 IPC이다. 

IPC란? 

IPCInter Process Communication의 줄임말로 프로세스 간의 통신 방법을 의미한다. 

내가 공부한 프로세스 간 통신 방법은 다음 그림으로 설명할 것이다.

이게 바로 파이프를 이용한 프로세스 간의 통신을 간략하게 그림으로 그려본 것이다.

파이프의 생성은 위와 같은 함수로 진행된다. 운영체제에서 생성되는 공간이기 때문에 프로세스들과는 무관한 독립된 공간이고, fork 시 코드에 포함이 되어 있더라도 복제되는 메모리 공간이 아니다.

예제 코드 및 실행 결과

어떻게 데이터를 주고 받는지 부모가 자식에게, 자식이 부모에게 데이터를 주고 받는 경우를 예제로 확인해 볼 것이다.

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 <unistd.h>
 
#define BUFSIZE 40
 
int main(void)
{
    int fds[2];
    pid_t pid;
 
    char from_c_to_p[] = "우리 부모님아!";
    char from_p_to_c[] = "내 자식아!";
 
    char buf[BUFSIZE];
 
    pipe(fds);
    // 파이프 생성 -> 파이프는 fork 에 의해 복사되지 않고, 운영체제에 의해 생성되는 메모리 공간이다.
    // 이 파이프를 이용하여 부모 프로세스와 자식 프로세스가 데이터를 교환할 수 있으며, 이런 통신 방법을 IPC 라고 한다.
    // 인자로 전달되는 fds에 데이터 입력과 출력에 해당하는 파일 디스크립터가 저장이 된다.
 
    // fds[0]에는 출력을 위한 파일 디스크립터가 저장되고, 프로세스는 이 디스크립터를 통해서 값을 가져온다.
    // fds[1]에는 입력을 위한 파일 디스크립터가 저장되고, 프로세스는 이 디스크립터를 통해서 값을 입력한다.
 
    pid = fork();
 
    if(pid == 0)
    {
        write(fds[1], from_c_to_p, sizeof(from_c_to_p));
       sleep(2);
 
        read(fds[0], buf, BUFSIZE);
        printf("부모가 자식에게 보내온 메시지 : [%s]\n", buf);
    }
    else
    {
        read(fds[0], buf, BUFSIZE);
        printf("자식이 부모에게 보내온 메시지 : [%s]\n", buf);
 
        write(fds[1], from_p_to_c, sizeof(from_p_to_c));
        sleep(5);
    }
    return 0;
}
cs

29번 라인과 40번 라인의 sleep 함수에 대한 설명이 필요한 것 같다ㅋㅋㅋ 먼저, 29번 라인은 자식 프로세스에서 실행되는 영역에 sleep 함수를 넣었다. 왜 넣었을까?

먼저, 29번 라인에 sleep 함수가 없다고 생각하고, 28번 라인에서 입력 파일 디스크립터를 이용하여 값을 파이프에 집어 넣으 후, 31번 라인에서 바로 출력 파일 디스크립터를 이용하여 값을 파이프에서 빼내면 자식 프로세스 본인이 방금 넣은 값이 나올 확률이 높다. 36번 라인에서 부모 프로세스도 출력 파일 디스크립터를 이용하여 파이프에서 값을 빼내고 있으므로 자식 프로세스에서의 31번 라인과 부모 프로세스에서의 36번 라인 중 어떤 라인을 먼저 읽느냐에 따라 달린 것이기 때문이다. 아래와 같이.

자식 프로세스가 파이프에 넣은 값을 자식 프로세스가 빼낸 모습

그러나 29번 라인에서 sleep 함수를 실행함으로써 부모 프로세스가 먼저 값을 가져갈 수 있도록 여유를 두면 정상적으로 실행된다 다음처럼.

정상적으로 실행된 모습

그리고 40번 라인의 부모 프로세스 실행 영역에서 5초 정도의 여유를 준 이유는 부모 프로세스가 종료하면 자식 프로세스도 함께 죽기 때문에 자식 프로세스가 충분히 부모가 전송한 데이터를 꺼낼 시간을 기다리기 위해서 5초의 여유를 준 것이다. 

 

그런데 이렇게 데이터를 넣고, 꺼내는 타임을 sleep 함수를 통해 실행 흐름까지 막아서 조정해가면서 행하는게 이게 프로그램으로서 말이나 된단 말인가?

아니, 그러면 애초에 부모용 pipe, 자식용 pipe 이렇게 구분해서

자식은 자식 파이프의 입력 디스크립터와, 부모 파이프의 출력 디스크립트만 사용하고,
부모는 부모 파이프의 입력 디스크립터와, 자식 파이프의 출력 디스크립트만 사용하면 된다.

다음 소스 코드와 실행 결과를 통해 볼 수 있다.

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
#include <stdio.h>
#include <unistd.h>
 
#define BUFSIZE 40
 
int main(void)
{
    int fds1[2], fds2[2]; // 순서대로 자식용 파이프, 부모용 파이프
    pid_t pid;
 
    char from_c_to_p[] = "우리 부모님아!";
    char from_p_to_c[] = "내 자식아!";
 
    char buf[BUFSIZE];
 
    pipe(fds1);
    pipe(fds2);
 
    pid = fork();
 
    if(pid == 0)
    {
        write(fds1[1], from_c_to_p, sizeof(from_c_to_p));
 
        read(fds2[0], buf, BUFSIZE);
        printf("부모가 자식에게 보내온 메시지 : [%s]\n", buf);
    }
    else
    {
        read(fds1[0], buf, BUFSIZE);
        printf("자식이 부모에게 보내온 메시지 : [%s]\n", buf);
 
        write(fds2[1], from_p_to_c, sizeof(from_p_to_c));
        sleep(5);
    }
    return 0;
}
cs

 

위와 같은 이런 모양새인 것이다.

이로써 IPC 까지 프로세스에 대한 내용은 모두 마친다.

728x90
반응형

+ Recent posts