본문 바로가기
일상,취미

다시 시작하는 TIL 5일차

by 진득한진드기 2024. 6. 11.

오늘 한일

오늘 드디어 엔진에서 멀티쓰레딩으로 파일을 가져오는 로직을 완성했다.

 

진짜 ㅋㅋㅋㅋ 유한 버퍼 문제 몇일간 잠도 못자고 고민 많이 했는데 얼추 비슷하게 해결해서 다행이다.....

 

원래 처음에는 그냥 버퍼 하나만 써서 하다가 그냥 버퍼 늘리면되는거 아닌가 생각했는데 생각만큼 간단하지 않았다 ㅠㅠㅠ

 

이거로 삽질을 너무 했다. 버퍼 갯수를 늘려도 결국에 동기화는 해야한다고 한다.(진작 물어볼걸)

 

버퍼 갯수로 잘 조절해서 써야되는데.... 이거는 아직은 내 영역 밖인거 같아서 일단 던져버렸다 ㅋㅋㅋㅋㅋ

 

동기화 자체가 어려워 유한버퍼 문제도 어렵게 보일 수 있으나 내가 이해한 방식으로 설명하겠다.

 

유한 버퍼문제는 다음과 같다.

 

유한버퍼 문제 : 다중 스레드 환경에서 발생하는 고전적인 동기화 문제 중 하나로 간단하게 생각해서 여러 쓰레드가 내가 사용하려는 고정된 크기에 버퍼에 접근할 때 우선순위와 접근 못하게 하는 등 동기화가 꼭 필요하다.(고마워요 지피티)

 

간단하게 생각해서 돌아갈 것 같은 루틴은 다음과 같다.

 

#include <stdio.h>

int buffer;
int count = o;

void put(int vaule){
    assert(count == 0);
    count = 1;
    return value;
}

int get(){
    assert(count == 1);
    count = 0;
    return buffer;
}

 

그냥 count가 1이면 값을 반환하고 count 가 0이면 버퍼인 Integer 값을 반환한다.

 

버퍼가 찼는지 확인 하고 값을 꺼낸 후 버퍼가 비었다고 설정하는 루틴으로 보면 될 것 같다.

 

#include <stdio.h>

int buffer;
int count = 0;

int put(int value) {
    assert(count == 0);
    count = 1;
    return value;
}
int get(){
    assert(count == 1);
    count = 0;
    return buffer;
}

void *produce(void *arg){
    int i;
    int loops = (int) arg;
    for(i = 0;i<loops;i++){
        put(i);
    }
}

void *consumer(void *arg){
    int i;
    while(1){
        int tmp = get();
        printf("%d \n",tmp);
    }
}

 

 

멀티 쓰레드로 위와 같은 코드가 있다고 하면 우리가 원하는 실행결과는 나오지 않는다.

 

이유는 경쟁상태가 되기 때문이다. 여기서 get()과 put()은 임계영역에 존재하게 된다.

 

이거는 단순히 락으로 보호한다고 해결되지 않는다.

 

상황이 복잡해지니까 간단하게 나눠서 봐보자.

 

크게 상황을 나눠보면 다음과 같이 나뉜다.

 

//생산자 코드

while(1){
 데이터 생산
 
 // 버퍼가 가득찼을 때의 대한 접근 동기화 가득차면 0이 된다.
    P(empty)
 
 // 버퍼에 대한 뮤텍스 
    P(mutex)감산
      버퍼에 데이터 넣기
 
 //버퍼에 대한 뮤텍스 
    V(mutex) 가산
 
 // 버퍼가 비었을때를 위한 접근 동기화 연산
    V(full) 
 
}

//소비자 코드

while(1){

// 버퍼가 빈 경우 동기화
    P(full)


//버퍼에 대한 접근
    P(mutex) 연산
      버퍼에 데이터 빼기
    V(mutex) 연산
// 버퍼에 대한 뮤텍스

//버퍼가 꽉 찼을때에 대한 접근 동기화
    V(empty) 

 데이터 소비
}

 

예시로 버퍼가 비었을 때를 가정 해보자 

 

초기 값은 mutex = 1, empty = n , full = 0

 

버퍼가 비었다고 하고 시작해보자 

 

P는 자원에 대한 소모이므로 lock을 한다고 보면되고 V는 가산이므로 unlock으로 보면 편하다.

 

1. 버퍼가 비면 소비자는 full이 0 이므로 감산이 되지 않는다. 즉 버퍼에 대한 데이터를 뺄 수 없게 된다. (쓰레드는 여기서 대기한다)

 

2. 반대로 생산자는 버퍼로 데이터를 넣는데 문제가 없기 때문에 다른 쓰레드는 버퍼에 대한 접근이 허락되고 데이터를 넣어준다.

 

3. 다 수행하고 나면 full가 가산된다. 즉 , n번 while문을 돌게 되므로 특정 횟수만큼 버퍼를 사용하게된다. 여기서 empty를 소비했으므로 다른 쓰레드가 접근하려고 할때 다른 쓰레드는 여기서 대기하게 된다.

 

4. 1번에서 대기 했던 쓰레드는 버퍼가 찼을때를 뜻하는 full이 가산되었으므로 실행하게 되고 우리가 원하는 버퍼에 대한 접근을 시도 할 수 있게 된다.

 

5. 버퍼에 대한 접근이 가능하다면 버퍼를 사용하게 된다. 그리고 이후에 버퍼가 꽉 찼을 때를 동기화 하는 empty변수를 가산(락을 풀고) 하여 버퍼를 비웠으니 사용해도 된다는 것을 알려준다.

 

5번에서 만약 다른 쓰레드가 접근하려고 하면 3번에서 말했듯 버퍼가 비어있으므로 대기상태가 된다.

 

 

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <mutex>
#include <condition_variable>

define MAX // 버퍼 사이즈 명시
int buffer[MAX];
int count = 0;
int loops;
int fill = 0;
int use = 0;
cond_t empty,fill;
mutex_t mutex;

void put(int value){
    buffer[fill] = value;
    fill = (fill + 1) % MAX;
    count++;
}

int get(){
    int tmp = buffer[use];
    use = (use + 1) % MAX;
    count--;
    return tmp;
}

void *producer(void *arg){
    int i;
    for(i = 0;i < loops;i++){
        pthread_mutex_lock(&mutex);
        if(count == 1) pthread_cond_wait(&empty,&mutex);
        put(i);
        pthread_cond_signal(&fill);
        pthread_mutex_unlock(&mutex);
    }
}

void *customer(void *arg){
    int i;
    for(i = 0;i<loops;i++){
        pthread_mutex_lock(&mutex);
        if(count == 0) pthread_cond_wait(&fill,&mutex);
        int tmp = get();
        pthread_cond_signal(&empty);
        pthread_mutex_unlock(&mutex);
        printf("%d\n",tmp);
    }
}

 

C언어로 생산자 소비자를 구현한 코드는 다음과 같다.

 

buffer를 배열 그리고 사이즈를 키우는 이유는 사이즈를 늘림으로서 병행성을 증가 시키고 더 효율적이게 된다.

 

버퍼가 확장되어 대기상태에 들어가기 전에 여러 값들이 생산될 수 있도록 하는 이유이다.

 

또 소비자 입장에선 버퍼크기가 증가하면 쓰레드 간의 문맥 교환(Context Switching)이 줄어들기 때문에 더 효율적이 된다.

 

그리고 이 코드에서 condition_variable을 쓴이유는 동기화에 더 적합한 방법이라 생각해서이다.

 

condition_variable은 특정 조건이 만족될 때까지 쓰레드를 대기 시킬 수 있는 큐 형식의 자료구조이다.

 

다른 쓰레드가 실행되면서 상태를 변경 시키고 해당 조건이 만족 되었을 때, 대기 중인 쓰레드를 깨워 실행하는 방식이다.

 

하나하나 다 락을 써서 구현하는 것 보다 더 직관적이고 관리가 편하다고 느껴진다.

 

이렇게 유한 버퍼 문제를 살펴봤다.

 

이 위 코드나 상황이 완전 이론적인 내용을 기반으로 해서 하나하나 뜯어보면서 보면 굉장히 어렵다 ㅋㅋㅋ ㅠ

 

근데 회사에서 사용하는 테스트엔진 코드는 레거시가 존재해서 처음부터 짤 필요는 없었고 그냥 논리적으로 맞게만 수정하고 동기화 관리만 잘하면 되는거 였는데 진짜 어디가 논리적으로 잘못됐는지 감도 안잡히고 너무 어려워서 정신 나갈뻔 했다.

아 물론 그렇다고 정신이 안나간건 아니다.

 

이거 하나 때문에 7일을 날렸으니;; 당연히 정신이 나갔다. 끝나고 생각하니까 아 맞네 이건데 진짜 그때 당시 보면 왜 안되는 지 조차 감도 안잡히는게..... 

 

또 생긴 문제

하나가 해결되면 그 뒤에 또 문제가 생긴다.

 

산 넘어 산이라는 말이 있다더니 어제 야매로 틀어막은 코드에서 문제가;; ㅋㅋ

 

생성한 프로세스에서 출력된 출력 값을 파싱하는 과정에서 문제가 생겼다.

 

표준 입출력이 나의 예상대로 나오지 않는다.(나한테 왜그래)

 

하나의 파일 기준으로 테스팅 해봤기에 당연히 다른 파일(프로그램) 도 그런줄 알았는데 다른 프로그램에서는 다른 출력 패턴이 보인다.

 

이게 진짜 랜덤이긴 한데 내가 볼때는 프로그램을 만들때 사용된 언어에 따라서 다른 것 같다.

 

프로그래밍언어에서 설정한 파싱되는 정책? 같은게 모두 다 다르니까 표준 입출력으로 나오는 값이 다 다른것 같다.

 

물론 생각해보면 맞는게 우리가 C++ 에서는 cout << 을 쓰던가 파이썬에선 print를 쓰던가 하는 입출력에 관한 로직이 모두 똑같다고 보기 힘들지 않은가 어떻게 보면 내가 멍청이가 아닐까

 

이거를 언어에 국한되지 않고 파싱해서 가져와야되는데 이게 진짜 문제다. 

 

아니면 모두 프로그래밍 언어에 국한되지 않고 사용할 수 있는 방식이 있을까?

 

내일이 두렵다.

 

책임님도 나의 역량을 확인한건지 IDA나 wireshark를 삭제해도 될 것 같다고 하신다.

 

ㅋㅋㅋㅋ 아니 이제야 나를 알아주는건가.... 역시 나는 버러지 였어....ㅋㅋㅋ

'일상,취미' 카테고리의 다른 글

다시 시작하는 TIL7일차  (1) 2024.06.13
다시 시작하는 TIL6일차  (1) 2024.06.12
다시 시작하는 TIL4일차....  (0) 2024.06.10
다시 시작하는 TIL3일차....  (1) 2024.06.07
다시 시작하는 TIL 2일차....  (0) 2024.06.06