동기화, 조건 변수 등 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

조건 변수

조건 변수를 설명하기 전에 다음과 같은 상황이 발생했다고 칩시다.

먼저 스레드 2개가 존재합니다. 저는 짧은 코드를 좋아하므로 아주 간단한 역할을 하는 2개의 쓰레드를 생성했습니다. 

Thread1 Thread2

data값 +1 증가

값을 출력


이때 thread2는 항상 thread1이 data값을 바꾼 다음에만 출력해야되는 조건이 있다면 이런 상황을 어떻게 구현해야할까요?

 

첫번째 방법은 우선 thread2는 항상 data가 변경되는 것을 지속적으로 감시한 후에 출력하면 되겠죠. 이런 해결 방법을 busy-waiting 또는 spinning이라고 합니다. 바쁜 대기라는 것입니다. 하는 일은 없는데 바쁘게 기다리고 있는 상황입니다. 무한루프로 변경을 감지하게 되는 것이라서 CPU의 점유율을 차지하게 됩니다.

 

그렇지 않고 특정 조건이 발생했을때 signal을 보내서 감지할 수도 있습니다. 이때 사용하는게 바로 조건 변수입니다. 

 

우리는 위와 같은 상황을 코드로 구현하기 전 우리가 사용해야하는 도구들을 먼저 간단히 보도록 하겠습니다.

 

pthread_cond_init

조건 변수를 초기화합니다. 이 함수말고 정적으로 조건 변수를 초기화할 경우에는 PTHREAD_CONT_INITIALIZER 상수를 이용해서 초기화할 수도 있습니다.

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

cond라는 조건 변수를 초기화하는데 attr로 속성을 지정할 수 있습니다. NULL이면 기본 조건 변수를 사용합니다.

 

pthread_cond_wait

조건이 참이 될 때까지 대기하는 함수입니다. pthread_cond_wait의 두번째 인수는 조건 변수를 보호하기 위한 뮤텍스입니다. pthread_cond_wait을 호출하기 전에 전달할 mutex를 이용하여 잠근 후에 이 함수를 호출해야합니다. 즉, pthread_cond_wait전에 pthread_mutex_lock을 호출하는데 둘의 mutex는 같아야한다는 것입니다. 

그러면 이 함수는 호출한 스레드를 조건의 발생을 대기하는 스레드들의 목록에 추가하고 뮤텍스를 풀게됩니다.

int pthread_cond_wait( pthread_cond_t* cond, pthread_mutex_t* mutex );

 

여기서 첫번째 인자가 condition 변수이고, 두번째 인자는 동기화를 할mutex입니다.

 

pthread_cond_signal

대기 중인 스레드에게 signal을 보냅니다. 현재 pthread_cond_wait으로 대기중인 스레드를 깨우게 되어 다른 스레드가 이후의 작업을 진행할 수 있도록 해줍니다. pthread_cond_wait과는 다르게 mutex를 받지 않음을 보세요.

int pthread_cond_signal(pthread_cond_t *cond);

 

이제 이것을 바탕으로 위의 문제점을 해결해보도록 해보지요. 아래의 소스 코드가 그것입니다.

코드 설명 : 아래의 코드는 1초마다 data라는 변수의 값을 증가시키는 thread1과 그 값을 단순히 출력해주는 thread2가 존재합니다. 

thread1은 값을 증가할때마다 thread2에게 출력을 하라고 pthread_cond_signal로 신호를 보냅니다.

data는 공유자원이기 때문에 mutex를 사용했습니다. 여기서 데이터의 조작은 한쪽에서 이루어지긴하지만 공유자원의 조작이 여러곳에서 이루어질 수도 있기 때문에 mutex로 동기화 처리를 하였습니다. 

 

쓰레드 동기화 기법에 대한 설명은 지난 포스팅을 참고하시기 바랍니다.

#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>

pthread_mutex_t mutex;
pthread_cond_t cond;

int data=0;

void *increase(void *arg){
        while(1){
                pthread_mutex_lock(&mutex);
                pthread_cond_signal(&cond);
                data++;
                pthread_mutex_unlock(&mutex);
                sleep(1);
        }
}

void *printData(void *arg){
        while(1){
                pthread_mutex_lock(&mutex);
                pthread_cond_wait(&cond,&mutex);
                printf("data :%d\n",data);
                pthread_mutex_unlock(&mutex);
        }
}
int main()
{
    pthread_t thread1,thread2;

    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    pthread_create(&thread1, NULL, increase,NULL);
    pthread_create(&thread2, NULL, printData,NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

 

이제 아래의 명령으로 컴파일 후 실행 결과를 보도록 하겠습니다. 결과를 보게되면 1초마다 값이 증가하는 것을 볼 수 있습니다.

# ./a.out
data :1
data :2
data :3
data :4
data :5

 

생산자-소비자 예제

한가지 예제를 더 살펴보도록 합시다. 생산자-소비자의 예제를 조건변수를 사용하여 구현한 코드입니다.

 

코드 설명:

코드가 길어보이지만 큐(queue)라는 자료구조를 아신다면 코드의 절반은 큐의 구현입니다. 그 중에서도 원형큐죠?

 

producer, 생상자 - item을 생성하여 queue에 집어넣습니다. 아래 produce함수를 실행하는 thread이지요.

consumer, 소비자 - item을 큐에서 꺼내어 출력합니다. consume함수를 실행하는 thread입니다.

공통 -현상을 명확히 보기 위해 양쪽 스레드는 랜덤한 시간을 기다렸다가 생산, 소비하게 됩니다.

 

만약 큐가 비어져있으면 소비할 item이 없으니 생산자에게 아이템을 생산하라는 신호를 보냅니다. c_cond가 바로 이 조건 변수입니다.

만약 큐가 전부 다 찼다면 생상할 수 없으니 소비자에게 소비하라는 신호를 보냅니다. p_cond가 바로 그 조건 변수입니다. 

 

코드는 아래와 같습니다.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

#define Q_MAX       15 + 1

// ================ queue implementation =======================
typedef struct queue_t {
        int             data[Q_MAX];
        int             nitem;
        int             head;
        int             tail;

        pthread_mutex_t mutex;
        pthread_cond_t  c_cond;
        pthread_cond_t  p_cond;

}queue_t;

queue_t queue={
        .p_cond=PTHREAD_COND_INITIALIZER,
        .c_cond=PTHREAD_COND_INITIALIZER,
        .mutex=PTHREAD_MUTEX_INITIALIZER

};

int is_empty( ){
        return queue.nitem == 0;
}

int is_full( ){
        return queue.nitem ==  Q_MAX-1;
}

void put( int d ){

        queue.data[queue.head] = d;
        queue.head = (queue.head+1)%Q_MAX;
        queue.nitem++;
}

int get(){

        int temp =  queue.data[queue.tail];
        queue.tail = (queue.tail+1)%Q_MAX;
        queue.nitem--;
        return temp;
}

// ==============queue implementation end ====================

void * produce(void *arg){

        int i=0;
        while(1){

                pthread_mutex_lock(&queue.mutex);

                if(is_full()){
                        pthread_cond_wait(&queue.c_cond,&queue.mutex);
                }

                printf("produce:%d\n",i);

                put(i);
                pthread_cond_signal(&queue.p_cond);

                pthread_mutex_unlock(&queue.mutex);
                i++;

                if(i==100) break;

                usleep(rand()%1000);
        }
        return 0;

}



void * consume(void *arg){
        while(1){

                pthread_mutex_lock(&queue.mutex);

                if(is_empty()){
                        pthread_cond_wait(&queue.p_cond,&queue.mutex);
                }
                int item=get();

                pthread_cond_signal(&queue.c_cond);
                printf("\t\tconsume:%d\n",item);

                pthread_mutex_unlock(&queue.mutex);

                usleep(rand()%1000);
        }

        return 0;

}



int main(int argc, char **argv){

        int     n;
        pthread_t producer, consumer;
        srand(time(0));

        pthread_create(&producer, 0, &produce, 0);
        pthread_create(&consumer, 0, &consume, 0);

        pthread_join(producer, 0);
        pthread_join(consumer, 0);

        return 0;

}

 

자 이제 컴파일 후 결과를 봅시다.

# gcc producer_consumer.c -lpthread
# ./a.out
produce:0
                consume:0
produce:1
                consume:1


...


produce:93
produce:94
produce:95
                consume:93
produce:96
                consume:94
produce:97
                consume:95
produce:98
                consume:96
produce:99
                consume:97
                consume:98
                consume:99

 

이상으로 간단한 조건변수 사용법을 알아보았습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

동기화와 동기화를 이용한 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

임계 영역(Critical Section) 

mutex를 알아보기전에 우선 critical section(임계구역)부터 간단하게 알아보자면 critical section은 하나의 한 스레드만이 진입해야하는 특정 코드 구역을 말합니다. 다시 말해 공유자원의 변경이 일어날 수 있는 구간이 임계 영역입니다. 공유자원이라고 하면 여러가지가 있을 수 있는데 간단히 변수라고 생각하세요.

 

예를 들어볼까요? 자, 아래코드의 임계영역은 cnt=0으로 초기화하며 for루프를 실행하는 구역입니다. 여기에 공유자원은 cnt가 되지요. 스레드가 2개가 있고 차례대로 create하게 됩니다. 아래의 소스코드가 각각 스레드가 실행부가 됩니다. 이 코드의 실행 결과를 한번 예측해보세요. 

void *count(void *arg){
    int i;
    char* name = (char*)arg;

    //======== critical section =============
    cnt=0;
    for (i = 0; i <10; i++)
    {
        printf("%s cnt: %d\n", name,cnt);
        cnt++;
        usleep(1);
    }
    //========= critical section ============
}

 

우리의 예측은 이렇습니다. 

thread1이 count함수 실행 : cnt를 0으로 초기화하고 cnt를 10번 증가시킨 후 종료

thread2가 count함수 실행 : cnt를 0으로 초기화하고 cnt를 10번 증가시킨 후 종료

 

하지만 실제 결과는 다르지요. 아래와 같이 뒤죽박죽으로 나옵니다.

thread2 cnt: 0
thread1 cnt: 0
thread1 cnt: 1
thread1 cnt: 2
thread2 cnt: 3
thread1 cnt: 4
thread2 cnt: 5
thread1 cnt: 6
thread1 cnt: 7
thread2 cnt: 8
thread2 cnt: 9
thread1 cnt: 10
thread2 cnt: 11
thread1 cnt: 12
thread1 cnt: 13
thread2 cnt: 14
thread1 cnt: 15
thread2 cnt: 16
thread2 cnt: 17
thread2 cnt: 18

 

뮤텍스(MutEx)

Mutual Exclusion의 약자로 상호배제라고 합니다. 특정 쓰레드 단독으로 들어가야되는 코드 구역에서 동기화를 위해 사용되는 동기화 기법입니다.

우리는 리눅스에서 이 뮤텍스를 통한 동기화를 수행하여 위 코드의 문제점을 해결해볼겁니다. 

우선 원래의 문제가 되는 모든 코드는 아래와 같습니다.

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int cnt=0;

void *count(void *arg){
    int i;
    char* name = (char*)arg;
    
    //======== critical section =============
    cnt=0;
    for (i = 0; i <10; i++)
    {
        printf("%s cnt: %d\n", name,cnt);
        cnt++;
        usleep(1);
    }
    //========= critical section ============
}

int main()
{
    pthread_t thread1,thread2;

    pthread_create(&thread1, NULL, count, (void *)"thread1");
    pthread_create(&thread2, NULL, count, (void *)"thread2");

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
}

 

문제해결을 위해서 우리가 생각해볼 수 있는 것은 cnt = 0 전에 먼저 실행되는 스레드가 어떤 잠금장치를 이용해 잠그고, 나올때 잠금을 해제하면 되겠다는 생각을 해볼 수 있겠네요. 이런 목적을 달성하기 위해 우리는 4개의 pthread mutex함수를 기억하면 됩니다. 이 함수들은 pthread.h내에 존재합니다.

 

pthread_mutex_init : mutex를 초기화하는데에는 두 가지 방법이 존재합니다.

 

1) 정적으로 할당된 뮤텍스를 초기화하려면 PTHREAD_MUTEX_INITIALIZER 상수를 이용해서 초기화합니다.

이런 형식으로 사용합니다. : pthread_mutex_t lock = PTHREAD_MUTX_INITIALIZER;

2) 동적으로 초기화하려면 pthread_mutex_init 함수를 사용하면 됩니다. mutex를 사용하기 전에 초기화를 시작해야합니다. 

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

 

첫번째 인자는 mutex, 두번째인자는 이 mutex의 속정을 주는데, 기본적으로 NULL을 사용합니다.

 

pthread_mutex_lock, pthread_mutex_unlock : 이 두 함수는 mutex를 이용하여 임계 구역을 진입할때 그 코드 구역을 잠그고 다시 임계 구역이 끝날때 다시 풀어 다음 스레드가 진입할 수 있도록 합니다.

 

한 가지 중요한 점은 pthread_mutex_lock이 어떤 스레드에서 호출되어 lock이 걸렸을때 다른 스레드가 임계구역에 진입하기 위해서 pthread_mutex_lock을 호출했다면 그 스레드는 이 전의 스레드가 임계 구역을 나올때까지, 즉, pthread_mutex_unlock을 할때까지 기다려야합니다. 

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

 

pthread_mutex_destroy : 만약 뮤텍스를 동적으로 생성(pthread_mutex_init을 이용하여 초기화)했다면 이 함수를 사용하는 함수가 pthread_mutex_destroy입니다.

int pthread_mutex_destroy(pthread_mutex_t *mutex);

 

이제 문제를 해결하는 코드를 봐야겠네요.

문제를 해결한 코드는 아래와 같습니다. 

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

pthread_mutex_t mutex;
int cnt=0;

void *count(void *arg){
    int i;
    char* name = (char*)arg;

    pthread_mutex_lock(&mutex);

    //======== critical section =============
    cnt=0;
    for (i = 0; i <10; i++)
    {
        printf("%s cnt: %d\n", name,cnt);
        cnt++;
        usleep(1);
    }
    //========= critical section ============
    pthread_mutex_unlock(&mutex);
}

int main()
{
    pthread_t thread1,thread2;

    pthread_mutex_init(&mutex,NULL);

    pthread_create(&thread1, NULL, count, (void *)"thread1");
    pthread_create(&thread2, NULL, count, (void *)"thread2");

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    pthread_mutex_destroy(&mutex);
}

 

critical section 전, 후에 lock, unlock을 하는 것과 프로그램 시작 직후, 종료 직전에 mutex를 초기화하고 제거하는 과정만 추가되었습니다. 

이제 컴파일하고 실행결과를 보도록 합시다.

#gcc pthread_mutex.c -lpthread
# ./a.out
thread2 cnt: 0
thread2 cnt: 1
thread2 cnt: 2
thread2 cnt: 3
thread2 cnt: 4
thread2 cnt: 5
thread2 cnt: 6
thread2 cnt: 7
thread2 cnt: 8
thread2 cnt: 9
thread1 cnt: 0
thread1 cnt: 1
thread1 cnt: 2
thread1 cnt: 3
thread1 cnt: 4
thread1 cnt: 5
thread1 cnt: 6
thread1 cnt: 7
thread1 cnt: 8
thread1 cnt: 9

 

차례대로 들어간 스레드부터 0~9까지 출력하는 것을 볼 수 있습니다. 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,