조건 변수를 사용하는 이유

    이전 글(https://shyeon.tistory.com/48)에서 mutex를 이용하여 스레드간 동기화를 하는 방법을 알아 보았다. 하지만 mutex만을 이용하여 동기화를 하는 것은 프로세스가 mutex변수를 지속적으로 검사해야 한다는 단점이 있다. 따라서 이 글에서 다루는 조건 변수(condition)을 함께 사용한다. 

 

함수

함수는 mutex함수와 굉장히 유사하다.

1. pthread_cond_init

int pthread_cond_init(pthread_cond_t *restrict condition, pthread_condattr_t *restrict attr);

mutex변수를 초기화하는 것처럼 cond변수도 초기화를 해야한다. 첫 번째 인자는 초기화하려는 cond 변수의 주소값을 넘기고 두 번째 인자는 cond 변수의 속성을 설정하는 변수이고 리눅스에선 무시하므로 NULL값을 주로 넣는다.

mutex 변수처럼 pthread_cond_init함수 말고 직점 변수에 PTHREAD_COND_INITIALIZER를 대입하여 초기화하는 방법도 있다.

 

2. pthread_cond_destroy

int pthread_cond_destroy(pthread_cond_t *condition);

mutex 변수와 마찬가지로 사용이 끝난 cond 변수는 pthread_cond_destroy함수를 호출하여 해제해줘야 한다.

 

3. pthread_cond_signal

int pthread_cond_signal(pthread_cond_t *cond);

cond 변수에 신호를 보내어 cond 변수를 기다리고 있는 스레드를 다시 시작시키는 함수이다. 만약 cond 변수를 기다리는 스레드가 없다면 아무 일도 일어나지 않느다. pthread_cond_signal함수는 cond 변수를 기다리는 여러 스레드 중에 단 하나만 다시 시작시킨다. 하지만 어떤 스레드를 시작시킬지는 지정할 수 없다.

 

4. pthread_cond_broadcast

int pthread_cond_broadcast(pthread_cond_t *cond);

하나의 스레드만 다시 시작시키는 pthread_cond_signal 함수와 다르게 cond 변수를 기다리고 있는 모든 스레드를 다시 시작시킨다.

 

5. pthread_cond_wait

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

cond 변수가 신호를 받을 때까지 기다리는 함수이다. 이 함수가 실행되면 mutex를 lock을 해제하고 기다린다. 따라서 다른 스레드에서는 mutex변수의 lock을 얻을 수 있다. 그 다음 이 함수를 벗어날 경우 mutex lock을 얻게 된다. 기다리는 시간을 지정할 수 있는 pthread_cond_timedwait도 있다.

 

예제

이 예제는 1부터 10까지의 숫자를 두 개의 스레드에서 출력한다. 1~3은 1번 스레드가, 4~7은 2번 스레드가, 8~10은 다시 1번 스레드가 실행한다. 1번 스레드가 실행할 부분은 2번 스레드에서 신호를 주어 cond변수를 이용하여 다시시작한다.

 

소스 코드

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //변수 초기화
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int value = 0; //공유 변수

void *func1(void *arg) { //스레드1이 실행할 함수
	while(1) {
		pthread_mutex_lock(&mutex); //lock을 걸고
		pthread_cond_wait(&cond, &mutex); //기다림
		value++; //1 증가시키고
		printf("thread1 : %d\n", value); //출력

		pthread_mutex_unlock(&mutex); //lock 해제

		if(value >= 10) //10을 넘어갈 경우 종료
			return NULL;
	}
}

void *func2(void *arg) { //스레드2가 실행할 함수
	while(1) {
		pthread_mutex_lock(&mutex); //lock을 걸고

		if(value < 3 || value > 6) { //1번 스레드가 실행할 부분
			pthread_cond_signal(&cond); //cond변수를 기다리는 스레드 1번을 다시 시작시킴
		}
		else { //그 외의 부분은
			value++; //1 증가시키고
			printf("thread2 : %d\n", value); //출력
		}

		pthread_mutex_unlock(&mutex); //lock 해제

		if(value >= 10)
			return NULL;
	}
}

int main() {
	pthread_t tid1, tid2;

	pthread_create(&tid1, NULL, &func1, NULL); //스레드 1 생성
	pthread_create(&tid2, NULL, &func2, NULL); //스레드 2 생성

	pthread_join(tid1, NULL); //스레드1 리소스 반환
	pthread_join(tid2, NULL); //스레드2 리소스 반환

	pthread_mutex_destroy(&mutex); //mutex 변수 해제
	pthread_cond_destroy(&cond); //cond 변수 해제

	return 0;
}

 

실행 결과

1~3, 8~9는 1번 스레드가 출력하고 4~7은 2번 스레드가 출력하는 것을 볼 수 있다.

Mutex란?

  • Mutual exclusion의 줄임말이다. lock을 건다는 말과 같다.
  • 스레드간에 동기화와 여러 스레드에서 같은 메모리에 쓸 때 공유되는 데이터를 보호한다.
  • mutex 변수는 공유 데이터를 접근하는 것을 보호하기 위해 lock처럼 사용된다.
  • mutex변수를 공유하는 스레드 중 하나의 스레드만이 mutex 변수에 lock을 걸 수 있다.
  • 공유된 변수를 번갈아가며 사용할 때 사용된다 (race condition 방지)

Mutex의 사용 예시

위 그림과 같이 은행 데이터베이스에 shyeon은 1000원, gildong은 1500원이 있다고 하자. 이때 A에서 shyeon에게 500원을 송금하고자하고 동시에 B에서 700원을 송금하고자 한다. 이때 무슨 일이 일어날까? 각각 A, B에서는 이런 일이 발생할 것이다.

  • 은행 DB로부터 shyeon의 계좌 정보를 변수에 저장한다.
  • 그 변수에 500원을 더한다.
  • 은행 DB에 계산 결과값을 저장한다.

A, B에서 위의 일이 동시에 발생한다면 A, B 모두 은행 계좌로부터 shyeon의 돈은 1000원이 있다고 읽어오고 A는 1000원에 500원을 더한 1500원을 은행 DB에 보낼 것이고 B는 700원을 더한 1700원을 DB에 보낼 것이다. 이때 더 늦게 도착하는 값으로 DB에 있는 shyeon의 잔고가 갱신될 것이다. 이런 상황을 race condition이라고 말한다. 어떤게 먼저 도착하냐에 따라 값이 달라지는 상황이다. 이런 일을 방지하려면 어떻게 해야할까? A나 B가 순서를 정하여 DB에 접근하면 된다. 이런 순서를 정하기 위해 mutex가 사용된다.

 

mutex 관련 함수

1. pthread_mutex_init

int pthread_mutex_init(ptrhead_mutex_t* mutex, const pthread_mutexatt_t *attr);

    mutex변수를 초기화하는데 사용하는 변수이다. mutex변수를 선언하여 첫 번째 인자에 주소값을 넘겨 초기화 할 수 있다. attr인자는 NULL로 하면 가본 속성으로 생성할 수 있다. 리눅스에서는 기본값인 NULL값을 사용한다. mutex변수를 초기화하는 또다른 방법으로는 다음과 같이 선언하면 된다.

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    위의 코드처럼 mutex변수에 직접 대입하는 방법이다. 이 방법은 attr인자를 사용할 수 없다는 단점이 있지만 리눅스에서는 고려하지 않는다.

 

2. pthread_mutex_destroy

int pthread_mutex_destroy(pthread_mutex_t *mutex);

    사용이 끝난 mutex 변수를 해제하는 함수이다. malloc으로 동적으로 선언한 mutex 변수는 free()를 호출하기 전에 꼭 pthread_mutex_destroy()를 먼저 호출해야한다.

 

3. pthread_mutex_lock

int pthread_mutex_lock(pthread_mutex_t *mutex);

    mutex 변수에 lock을 거는 함수이다. 두 스레드의 함수에서 같은 mutex변수에 대해 각각 pthread_mutex_lock을 호출할 경우 두 스레드 중 하나의 스레드만 mutex 변수에 대해 lock을 얻고 다른 스레드에서는 pthread_mutex_lock에서 기다리고 있다. 이때 mutex를 얻은 스레드에서 pthread_mutex_unlock()을 호출할 경우 다른 스레드에서 mutex변수에 lock을 걸 수 있다.

 

예제

아래 코드는 두 스레드에서 공유하는 하나의 변수에 번갈아가며 출력하는 예제이다.

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int value;

void *func1(void *arg) { //1번 스레드가 실행할 함수
	for(int  i = 0; i < 5; i++) {
		pthread_mutex_lock(&mutex); //mutex 변수 lock(다른 변수에서 접근 불가능)
		printf("thread1 : %d\n", value); //변수 출력
		value++; //변수 증가
		pthread_mutex_unlock(&mutex); //mutex 변수 unlock(다른 변수에서도 접근 가능)
		sleep(1);
	}

	pthread_exit(NULL); //스레드 함수 종료
}

void *func2(void *arg) { //2번 스레드가 실행할 함수
	for(int i = 0; i < 5; i++) { 
		pthread_mutex_lock(&mutex); //mutex 변수 lock(다른 변수에서 접근 불가능)
		printf("thread2 : %d\n", value); //변수 출력
		value++; //변수 증가
		pthread_mutex_unlock(&mutex); //mutex 변수 unlock(다른 변수에서 접근 가능)
		sleep(1);
	}

	pthread_exit(NULL); //스레드 함수 종료
}

int main() {
	pthread_t tid1, tid2; //스레드 변수 선언

	value = 0; //공유 변수 초기화

	if(pthread_create(&tid1, NULL, func1, NULL) != 0) { //1번 스레드 생성
		fprintf(stderr, "pthread create error\n");
		exit(1);
	}

	if(pthread_create(&tid2, NULL, func2, NULL) != 0) { //2번 스레드 생성
		fprintf(stderr, "pthread create error\n");
		exit(1);
	}

	if(pthread_join(tid1, NULL) != 0) { //1번 스레드 종료 후 리소스 회수
		fprintf(stderr, "pthread join error\n");
		exit(1);
	}

	if(pthread_join(tid2, NULL) != 0) { //2번 스레드 종료 후 리소스 회수
		fprintf(stderr, "pthread join error\n");
		exit(1);
	}

	pthread_mutex_destroy(&mutex); //mutex 변수 해제

	exit(0);
}

 

실행 결과

 

mutex lock 실행 결과
mutex lock 주석처리 후 실행 결과

    첫 번째 사진은 mutex lock을 사용한 결과이고 두 번째 사진은 pthread_mutex_lock(), pthread_mutex_unlock()부분을 주석처리하고 실행한 결과이다. 실행 결과 1번 사진과 같이 1번 스레드와 2번 스레드가 번갈아가며 변수를 증가하며 출력하는 것을 볼 수있다. 2번 사진은 공유된 변수에 동시에 접근하여 같이 출력되는 것을 알 수 있다. 이렇게 여러 스레드가 공유 변수에 접근하고자할 때 race condition을 방지하기 위해 mutex를 사용할 수 있다.

 

    pthread 라이브러리를 사용할 때는 컴파일할 때 -lpthread를 포함해줘야한다. 그렇지 않으면 에러가 발생하고 컴파일이 되지 않는다.

 

개발 환경

wsl2 Ubuntu-20.04

gcc (Ubuntu 9.3.0-17ubuntu1~20.04)

+ Recent posts