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

진짜 다 까먹어서 다시 예제 쳐보고 다시 올린다... 정말 미리미리 올리고 자주자주 봐야합니다..

포스팅 순서가 사실 멀티 프로세싱이 먼저이지만, 게임 서버 개발에 사용한 것은 멀티 플렉싱 모델이었기 때문에 먼저 올렸었는데, 사실 멀티 프로세싱이 무엇인지, 그리고 그 단점이나 필요한 영역은 어느 부분인지 알아야 한다.

그래서 포스팅을 또 하게 되었다.

내가 구현했던 서버의 모델은 멀티 플렉싱 모델이었다(select, epoll) 이 모델은 위와 같은 구조로 이루어진다.

여러 학생들이 클라이언트들이고, 교사하나프로세스이다.
질의와 답변을 필요로 하는 학생들, 즉 송수신을 요청하는 애들만 빠른 반복문 처리를 통해 처리한다.

이러한 모델에서는 여러 명의 클라이언트들이 송수신이나 연결 등의 여러 요청을 할 때마다 그 요청만큼만 처리하는 구조이기 때문에 대용량의 파일을 송수신하는 연결과는 적합하지 않다. 

반면, 멀티 프로세싱 모델은 다음과 같다.

교사들 프로세스들이고, 여러 학생들이 클라이언트들인것이다.
학생들이 질의와 답변을 필요로 하든 안 하든 무조건 프로세스와 연결되어 1대1 통신이 진행되는 것이다.

이런 구조에서는 시간이 오래 걸리는 상담이나 컨설팅 같은 서비스가 교사와 학생들 사이에서 제공되어야 할 법하다. 마찬가지로 프로세스-클라이언트 입장에서 봤을 때, 용량이 크고 시간이 오래 걸리는 파일 전송 등이 자원 효율이 클 것이다.

이러한 멀티 프로세싱 기반의 소켓을 작성하기 위해서는 fork() 에 대한 개념이 필요하다.

그전에! 프로세스란 무엇인가?

운영체제 관점에서 별도의 실행 흐름을 구성하는 단위. 
생성될 때, PCB(Process Controll Block) 라고 하는 프로세스에 대한 정보 구조가 생성되며,
이 정보에는 해당 프로세스에 대한 구체적인 정보들이 저장된다
(메모리 할당, 파일 디스크립터 테이블 등등 수 많은..)

프로세스의 복사본을 생성하는 fork()

위와 같은 기본 코드가 있다. 10번 줄을 보면 fork 라는 함수를 호출한다. 이 fork() 함수프로세스를 그대로 복사하는 함수이다. 이 함수가 호출되는 순간!! 프로세스가 그대~로 복사되어 10번 라인부터는 흐름이 각각 따로 진행된다. 다음과 같이 말이다.

그렇다면 이 함수가 반환하는 값은 무엇일까? 원래 실행되던 흐름부모 프로세스라고 하고, 복사된 실행 흐름자식 프로세스라고 한다.

부모 프로세스의 실행 흐름일 때, 10번 라인에서 반환되는 값은 자식 프로세스의 프로세스 ID 번호이다.
자식 프로세스의 실행 흐름일 때, 10번 라인에서 반환되는 값은 무조건 0이다.

그래서 12번 라인과 16번 라인에서는 부모 프로세스와 자식 프로세스의 실행 흐름을 구별하기 위해 pid를 0인지 아닌지를 비교하고 각 실행 흐름에 맞는 처리를 진행하면 된다.

 

이런 식으로 프로세스를 생성하므로, 클라이언트가 접속하면 프로세스를 복사하여 클라이언트에게 할당해줘서 송수신처리를 담당하면 된다. 

그런데.. 이러한 방식에는 큰 문제가 있었으니..

 

이번 포스팅에서 제일 중요한 것은 프로세스의 복사는 아예 메모리까지 통채로 복사되어
실행 흐름이 완전 다른 별개로 나뉘어진 부모 프로세스, 자식 프로세스라는 것이다!

728x90
반응형

+ Recent posts