reference: "Mastering C++ Multithreading" / 마야 포쉬
pthread_mutex_
또는 pthread_mutexattr_
접두어가 붙는 함수를 통해 뮤텍스와 그 속성 객체를 작동한다.
pthreads에서 뮤텍스는 초기화되고, 해제, 락, 언락이 이루어진다. 이들은 pthread_mutexattr_t
구조체를 사용해 커스텀화된 동작을 한다. 이 구조체는 또한 자신에 대한 속성을 초기화하고 해제하는 것에 해당하는 pthread_mutexattr_*
함수를 가진다.
// 정적 초기화를 사용하는 pthread 뮤텍스의 기본적 사용 방법
static pthread_mutex_t func_mutex = PTHREAD_MUTEX_INITIALIZER;
void func(){
pthread_mutex_lock(&fucn_mutex);
..
..
pthreaed_mutex_unlock(&func_mutex);
}
위 코드에서 'PTHREAD_MUTEX_INITIALIZER' 매크로를 사용해 매번 뮤텍스에 대한 코드를 입력하지 않고서도 뮤텍스를 초기화한다. 매크로의 사용이 도움이 되긴 하지만 다른 API와 비교해 수동으로 뮤텍스를 초기화하고 해제해야 한다.
조건 변수는 pthread_cond_
또는 pthread_condattr_
접두어가 붙은 함수들이다. 이들 함수는 조건 변수와 그 속성 객체에 작동한다.
pthreads에서 조건 변수는 초기화와 해제 함수를 가지는 경우와 동일한 패턴을 따르며 또한 pthread_condattr_t
속성 구조체를 관리하는 것과 동일한 패턴을 따른다.
// pthreads 조건 변수의 기본적 사용법
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#define COUNT_TRIGGER 10
#define COUNT_LIMIT 12
int count = 0;
int thread_ids[3] = {0, 1, 2};
pthread_mutex_t count_mutex;
pthread_cond_t count_cv;
이전 코드에는 표준 헤더 파일이 보이고 그 용도는 잠시 뒤 보게 될 COUNT_TRIGGER와 COUNT_LIMIT가 정의되어 있다. count 변수와 생성하고자 하는 쓰레드 ID, 뮤텍스와 조건 변수 같은 전역 변수도 정의되어 있다.
void* add_count(void* t){
int tid = (long)t;
for(int i=0; i<COUNT_TRIGGER; i++){
pthread_mutex_lock(&count_mutex);
count++;
if(count == COUNT_LIMIT){
pthread_cond_signal(&count_cv);
}
pthread_mutex_unlock(&count_mutex);
sleep(1);
}
pthread_exit(0);
}
add_count 함수에서는 기본적으로 count_mutex로서 전역 카운터 변수에 대한 배타적 접근을 획득한 이후 이 변수의 값을 증가시킨다.
이 함수를 실행하는 두 번째 쓰레드에게 뮤텍스를 획득할 기회를 주기 위해 루프를 돌 때마다 1초 동안 sleep한다.
void* watch_count(void* t){
int tid = (int)t;
pthread_mutex_lock(&count_mutex);
if(count < COUNT_LIMIT){
pthread_cond_wait(&count_cv, &count_mutex);
}
pthread_mutex_unlock(&count_mutex);
pthread_exit(0);
}
watch_count 함수에서는 카운트 제한에 도달했는지를 검사하기 전에 전역 뮤텍스를 lock 시킨다. 이것은 카운트가 제한 값에 도달하기 전에 이 함수를 실행하는 쓰레드가 호출되지 않는 경우를 대비한 것이다.
그렇지 않다면 조건 변수와 locked된 뮤텍스를 제공하는 조건 변수를 대기한다. 시그널이 되면 전역 뮤텍스를 unlock시키고 쓰레드는 종료한다.
int main(int argc, char* argv[]){
int tid1 = 1, tid2 = 2, tid3 = 3;
pthread_t threads[3];
pthread_attr_t attr;
// 뮤텍스 변수 초기화
pthread_mutex_init(&count_mutex, 0);
// 조건 변수 초기화
pthread_cond_init(&count_cv, 0);
// attr 구조체 초기화
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
// 쓰레드 생성
pthread_create(&threads[0], &attr, watch_count, (void *)tid1);
pthread_create(&threads[1], &attr, add_count, (void *)tid2);
pthread_create(&threads[2], &attr, add_count, (void *)tid3);
for(int i=0; i<3; i++){
pthread_join(threads[i], 0);
}
pthread_attr_destroy(&attr);
pthread_mutex_destroy(&count_mutex);
pthread_cond_destroy(&count_cv);
return 0;
}
메인 함수에서 3개의 쓰레드를 생성한다. 이들 중 두 쓰레드는 카운터를 증가시키는 함수를 실행하고, 나머지 하나의 쓰레드는 조건 변수가 시그널되기를 대기하는 함수를 실행한다.
또한 메인 함수에서 전역 뮤텍스와 전역 변수를 초기화한다. 생성되는 쓰레드는 "합류 가능(joinable)" 속성이 명시적으로 설정된다.
끝으로 각 쓰레드가 마치기를 대기하고, 종료 전 속성 구조체 인스턴스와 뮤텍스 그리고 조건 변수를 해제하여 정리 작업을 수행한다.
pthread_cond_broadcast()
위 함수를 사용하면 큐에 있는 첫 번째 쓰레드만 시그널 시키는 것이 아니라 조건 변수를 대기하는 모든 쓰레드를 시그널시킬 수 있다. 이를 통해 새로운 데이터 설정을 대기하는 작업자 쓰레드가 많은 경우 개별 쓰레드별로 통지하지 않아도 되는 방식으로 일부 애플리케이션에서는 좀 더 세련되게 조건 변수를 사용할 수 있다.