조건 변수를 사용하는 이유

    이전 글(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번 스레드가 출력하는 것을 볼 수 있다.

스레드란?

  • 프로세스 내의 제어 흐름
  • 일반적으로 우리가 작성하는 코드는 단일 스레드 단일 프로세스
  • 다중 스레드 프로세스는 하나의 프로세스에 여러 컨트롤이 존재함

쉽게 말해 스레드란 우리가 프로그램을 실행할 때 코드가 실행되는 흐름이라고 할 수 있다.

 

특징

  • 동일 프로세스에서 동작하는 여러 개의 스레드는 코드 영역, 데이터 영역, 리소스(파일 디스크립터, 시그널 등)를 공유한다.
  • 스레드는 각각 Program Counter(PC)를 가지고 있다 - 이는 생각해보면 당연하다. 각 스레드는 실행하는 코드의 위치가 다르다. 즉 실행하는 명령어가 다르다. 따라서 각각 PC를 가지고 있어야한다.
  • 스레드는 스택 영역, 레지스터 집합, 스레드 ID를  각자 가지고 있다.

 

예시

그러하면 실제로 다중 스레드는 어디에 이용될까? 우리가 사용하는 웹 브라우저에서도 다중 스레드는 사용된다. 예를 들어 이미지, 텍스트 등 화면에 홈페이지를 표시해주는 스레드가 1개 존재한다면 백엔드로부터 데이터를 읽어오는 역할을 수행하는 스레드가 따로 동작하고 있을 수 있다. 이렇게 동시에 여러 기능을 수행하려면 다중 스레드를 이용하여야 한다.

 

함수

리눅스에서 제공하느 pthread 시스템 호출 함수를 이용하여 다중 스레드를 사용할 수 있다. 그럼 스레드를 제어할 수 있는 함수는 어떤 것들이 있는지 간단히 살펴보겠다.

  • pthread_create()     
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);

 - 새로운 스레드를 생성하는 함수이다. 함수의 원형은 위의 코드와 같다.

 - 첫 번째 인자로 새로 생성할 스레드의 ID가 저장될 주소 값을 넘겨준다.

 - 두 번째 인자는 기본 특성을 가지는 스레드의 경우 NULL을 넣어주고 pthread_attr_init()으로 pthread_attr_t 구조체를 초기화하여 사용자 모드 스레드나 커널 모드 스레드를 사용할 수 있다.

 - 세 번째 인자는 새로 생성되는 스레드가 실행할 함수의 포인터를 준다.

 - 네 번째 인자는 세 번째 인자의 함수에게 넘기고 싶은 정보를 구조체에 저졍하여 그 포인터를 넘겨주어 스레드가 실행하는 함수에서 그 정보를 사용할 수 있게 한다.

  • pthread_exit()
void pthread_exit(void *rval_ptr);

  - 스레드를 종료시키는 함수. 인자로 준 rval_ptr 값은 pthread_join의 두번 째 인자에서 받을 수 있다. 만약 pthread_detach()를 하지 않은 경우에는 스레드가 종료되었어도 스레드의 자원은 회수되지 않는다.

  • pthread_join()
int pthread_join(pthread_t thread, void **rval_ptr);

 - main스레드가 생성한 스레드가 종료될 때까지 기다리는 함수. 종료된 스레드의 자원을 회수하는 역할을 한다.

 - pthread_join()을 호출한 스레드는 그 스레드가 pthread_exit()을 호출할 때까지 대기한다.

 - main스레드의 종료로 인해 다른 스레드들이 강제로 종료되는 것을 방지한다.

 - 첫 번째 인자는 종료를 기다리는 스레드의 id이다.

 - 두 번째 인자는 pthread_exit()에서 넘겨주는 인자와 같은 값이다. 만약 스레드가 정상적으로 종료되었다면 두 번째 인자에 리턴 코드가 저장되고 스레드가 취소되면 rval_ptr이 가리키는 곳이 PTHREAD_CANCELED가 설정된다.

 - 만약 pthread_create의 두 번째 인자인 pthread_attr_t의 인자로 NULL을 준 경우는 스레드를 join해야한다.

  • pthread_detach() 
int pthread_detach(pthread_t tid);

 - pthread_join과 다르게 메인에서 스레드를 기다리지 않는 함수.

 - 스레드가 종료될 경우 자동으로 자원이 회수된다. (pthread_join을 할 필요 없다)

 - phtread_detach한 경우 pthread_join으로 스레드를 기다릴 수 없다. 즉 joinable하지 않다.

예제

실제로 다중 스레드를 이용하여 모두 공유하는 전역변수를 2개의 스레드가 1씩 증가시키며 출력하는 프로그램이다.

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

int var = 0;

void *func1(void *arg) { //1번 스레드 실행 함수
	while(var < 10) {
		printf("thread1 : %d\n", ++var);
		sleep(1);
	}
	pthread_exit(NULL); //1번 스레드 종료
}

void *func2(void *arg) { //2번 스레드 실행 함수
	while(var < 10) {
		printf("thread2 : %d\n", ++var);
		sleep(1);
	}
	pthread_exit(NULL); //2번 스레드 종료
}


int main() {
	pthread_t tid1, tid2;

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

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

	pthread_join(tid1, NULL); //1번 스레드 자원 회수
	pthread_join(tid2, NULL); //2번 스레드 자원 회수

	return 0;
}

 

결과

위 코드 실행 결과

위 그림처럼 두 개의 스레드가 각각 공유하는 전역 변수를 1씩 증가시키며 출력하는 것을 볼 수 있다. 하지만 이상한 점이 있다. 스레드가 실행되는 순서가 일정하지 않다는 것이다. 이렇게 스레드가 실행되는 순서는 생성되는 순서와 전혀 관계가 없다. 스레드의 실행 순서를 제어하려면 동기화가 필요하다. 동기화는 이후에 다뤄보겠다.

 

개발환경

OS : wsl2 Ubuntu-20.04

gcc : Ubuntu 9.3.0-17ubuntu1~20.04

+ Recent posts