1. 컨디션 변수

  • “락” 이외에도 병행 프로그램을 제작할 수 있는 다른 기법들이 존재한다.
  • 쓰레드가 실행을 계속하기 전에, 특정 조건의 만족여부를 검사해야 하는 경우가 많이 있다.
    volatile int done = 0;
  
    void *child(void *arg){
        printf("child\n");
        done = 1;
        return NULL;
    }

    void main(int argc, char *argv[]){
        printf("parent: begin\n");
        pthread_t c;
        pthread_Create(&c, NULL, child, NULL);
        while(done==0); // spin-wait
        printf("parent: end\n");
        return 0;
    }
  • 위의 코드는 공유 변수를 사용한다.
    • while 문을 돌며 공유 변수가 변경될 때까지 기다린다.
    • 이 방법은 제대로 동작하지만 부모 쓰레드가 회전을 하면서 CPU 시간을 낭비하기 때문에 비효율적이다.

1.1 컨디션 변수의 개념과 관련 루틴

  • 쓰레드 실행시, 특정 조건이 만족될 때까지의 대기를 위해 컨디션 변수(conditional variable)라고 불리는 개념을 사용할 수 있다.
  • 컨디션 변수는 일종의 큐 자료 구조다.
  • 컨디션 변수는 쓰레드 실행에서 어떤 상태(또는 어떤 조건)가 원하는 것과 다를 때 조건이 만족되기를 대기하는 큐이다.
  • wait(): 쓰레드가 스스로를 잠재우기 위해서 호출
  • signal(): 조건이 만족되기를 대기하며 잠자고 있던 쓰레드를 깨울 때 호출
    int done = 0;
    pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_t c = PTHREAD_COND_INITIALIZER;
    
    void thr_exit() {
        pthread_mutex_lock(&m);
        done = 1;
        pthread_cond_signal(&c);
        pthread_mutex_unlock(&m);
    }
    
    void *child(void *arg){
        printf("child\n");
        thr_exit();
        return NULL;
    }
    
    void thr_join(){
        pthread_mutex_lock(&m);
        while (done ==0)
            pthread_cond_wait(&c, &m);
        pthread_mutex_unlock(&m);
    }
    
    int main(int argc, char *argv[]){
        printf("parent: begin\n");
        pthread_t p;
        pthread_create(&p, NULL, child, NULL);
        thr_join();
        printf("parent: end\n");
        return 0;
    }
  • wait()는 mutex를 인자로 받으며 wait()가 호출될 때 mutex는 잠겨있다고 가정한다.
  • wait()는 mutex를 해제하고 호출한 쓰레드를 재운다.
  • 다른 쓰레드가 시그널을 보내어 대기중인 쓰레드가 슬립(sleep) 상태에서 깨어나면, wait()에서 리턴하기 전에 반드시 락을 재획득해야 한다.

    “슬립에서 깨어난 프로세스는 리턴하기전에 락을 재획득해야한다.” <- 중요한 문장이다.

  • pthread_cond_wait(&c, &m) <- 함수의 호출을 통하여, 부모쓰레드는 자신의 상태를 대기로 변경함과 동시에 획득했던 락을 반납한다.
  • wait()에서 리턴 시, 부모 쓰레드는 락을 보유한 상태가 된다.
  • 잠자고, 깨우고, 락을 설정하는 것이 done 이라는 상태 변수를 중심으로 구현되어 있다.

1.2 생산자/소비자(유한 버퍼) 문제

  • Dijkstra가 처음 제시한 생산자/소비자(producer/consumer) 문제를 살펴볼 것이다.
    • 락이나 컨디션 변수를 대신하여 사용할 수 있는 일반화된 세마포어를 발명하게 된 이유가 이 생산자/소비자 문제 때문이다.

      생산자/소비자란, 다수의 생산자 쓰레드와 소비자 쓰레드가 있다고 하자. 생산자는 데이터를 만들어 버퍼에 넣고, 소비자는 버퍼에서 데이터를 꺼내어 사용한다.

  • 유한 버퍼는 공유 자원이다. 경쟁 조건의 발생을 방지하기 위해 동기화가 필요하다.
    int buffer;
    int count = 0;
  
    void put(int value) {
        assert(count == 0);
        count = 1;
        buffer = value;
    }
    
    void get(){
        assert(count == 1);
        count = 0;
        return buffer;
    }

불완전한 해답

  • put()과 get() 루틴에는 임계 영역이 존재한다.
    • put()은 버퍼에 내용을 기록하고, get()은 버퍼에 있는 내용을 읽는다.
  • 임계 영역을 락으로 보호하는 것만으로는 제대로 동작하지 않는다. 추가적인 장치가 필요하다. 추가적인 장치가 컨디션 변수이다.
  • if문을 사용해서 어느정도 문제를 해결할 수 있지만, 여전히 문제가 존재한다.
    • 쓰레드가 대기 상태에서 깨어나는 시점과 이 쓰레드가 실제로 실행되는 시점사이에 시차가 존재한다.
      • 이 기간동안 버퍼 상태가 변경될 수 있다.
  • 위의 문제를 해결하기 위해 깨어난 쓰레드가 실제 실행되는 시점에는 시그널을 받았떤 시점의 상태가 그대로 유지되어있는지를 다시 체크해야 한다.
    • 이런 식의 시그널을 정의하는 것을 Mesa semantic이라 한다.

개선된, 하지만 아직도 불완전한: if 문 대신 while 문

  • 간단하게 해결하기 위해서는 if 문을 while 문으로 바꾸면 된다.
    • if문은 처음만 조건을 계산한 이후 if문을 탈출하지만, while 문은 조건을 계속 계산해서 조건에 부합하지 않을 때 while문을 탈출한다.
  • while문을 사용해서 어느정도 문제를 해결할 수 있지만, 여전히 문제가 존재한다.
    • 여러 쓰레드가 존재할 때 깨워야할 쓰레드가 아닌 다른 쓰레드를 깨우게 되면 모두 대기 상태로 빠질 수도 있다.
    • 시그널을 받는 대상을 명시하는 것이 실질적으로 가능하지 않다. 우리가 원하는 것은 소비자는 생산자만을, 생산자는 소비자만을 깨우는 것이다.

단일 버퍼 생산자/소비자 해법

  • 두 개의 컨디션 변수를 사용하면 된다.
  • 생산자 쓰레드가 empty 조건 변수에서 대기하고 fill에 대해서 시그널을 발생한다.
  • 정반대로 소비자 쓰레드는 fill 에 대해서 대기하고 empty에 대해서 시그널을 발생시킨다.
    // 올바른 put()과 get() 루틴
    int MAX;
    int buffer[10];
    int fill = 0;
    int use = 0;
    int count = 0;
    
    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;
    }
    
    // 올바른 생산자/소비자 동기화
    pthread_cond_t empty_c, fill_c;
    pthread_mutex_t mutex;
    int loops = 10;
    
    void *producer(void *arg){
        int i;
        for (i=0; i< loops; i++){
            pthread_mutex_lock(&mutex);
            while(count == MAX)
                pthread_cond_wait(&empty_c, &mutex);
            put(i);
            pthread_cond_signal(&fill_c);
            pthread_mutex_unlock(&mutex);
        }
        return 0;
    }
    
    void *consumer(void *arg){
        int i;
        for(i=0; i<loops; i++){
            pthread_mutex_lock(&mutex);
            while(count == 0)
                pthread_cond_wait(&fill_c, &mutex);
            int tmp = get();
            pthread_cond_signal(&empty_c);
            pthread_mutex_unlock(&mutex);
            printf("%d\n", tmp);
        }
        return 0;
    }

태그: ,

카테고리:

업데이트:

댓글남기기