운영체제가 스레드 생성, 제어를 위해 제공하는 인터페이스에 대해 알아보자
멀티 스레드 프로그램을 작성하려면 우선 새로운 스레드를 생성해야한다. POSIX의 방법을 살펴보자
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void*),
void *arg);
pthread_create 함수에 4개의 인수가 있다.
1 #include <stdio.h>
2 #include <pthread.h>
3
4 typedef struct {
5 int a;
6 int b;
7 } myarg_t;
8
9 void *mythread(void *arg) {
10 myarg_t *args = (myarg_t *) arg;
11 printf("%d %d\n", args->a, args->b);
12 return NULL;
13 }
14
15 int main(int argc, char *argv[]) {
16 pthread_t p;
17 myarg_t args = { 10, 20 };
18
19 int rc = pthread_create(&p, NULL, mythread, &args);
20 ...
21 }
스레드의 완료를 기다리기 위해서 pthread_join()을 사용한다.
int pthread_join(pthread_t thread, void **value_ptr);
함수에 두 개의 인수가 있다.
1 typedef struct { int a; int b; } myarg_t;
2 typedef struct { int x; int y; } myret_t;
3
4 void *mythread(void *arg) {
5 myret_t *rvals = Malloc(sizeof(myret_t));
6 rvals->x = 1;
7 rvals->y = 2;
8 return (void *) rvals;
9 }
10
11 int main(int argc, char *argv[]) {
12 pthread_t p;
13 myret_t *rvals;
14 myarg_t args = { 10, 20 };
15 Pthread_create(&p, NULL, mythread, &args);
16 Pthread_join(p, (void **) &rvals);
17 printf("returned %d %d\n", rvals->x, rvals->y);
18 free(rvals);
19 return 0;
20 }
POSIX 스레드 라이브러리가 제공하는 함수인 Lock 은 critical section(임계 구역)에 mutual exclusion(상호 배제)을 제공한다.
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
이름이 락과 언락이니 한 쌍인 것을 눈치챘을 것이다. 이 루틴은 critical section인 코드가 있을때 유용하게 쓸 수 있다.
pthread_mutex_t lock;
pthread_mutex_lock(&lock);
x = x + 1;
pthread_mutex_unlock(&lock);
위와 같은 예제에서 pthread_mutex_lock을 호출한 스레드가 락을 보유하지 않으면 스레드는 락을 획득하고 critical section에 들어간다. 락을 이미 보유한 다른 스레드가 있으면 락을 가져올 때 까지 호출에서 반환되지 않는다. 락을 보유한 스레드가 unlock()호출을 해야한다는 것이다.
사실 락은 적절하게 초기화 되어야 한다.
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
이렇게 하면 락이 기본값으로 설정되고 락을 사용할 수 있게 된다. 동적으로 하려면 다음과 같이 하면 된다.
int rc = pthread_mutex_init(&lock, NULL);
assert(rc == 0); // 항상 성공 확인!
이 루틴의 첫 번째 인수는 락의 주소, 두 번째 인수는 속성 세트이다.(null은 기본값)
하지만 문제는 lock,unlock 호출 시 오류코드를 확인하지 않는 것이다. 따라서 실패가 발생할 수 있으며 여러 스레드가 ciritical section에 들어갈 수 있다.
이 문제를 해결하기 위한 루틴이 있다.
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *mutex, struct timespec *abs_timeout);
trylock은 락이 이미 보유되어 있으면 실패를 반환하며,
timelock은 시간이 지난후나 락을 획득한 후에 반환한다.
조건 변수(pthread_cond_t)는 스레드 간 신호를 주고 받을 때 유용하다.
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
조건 변수를 사용하려면 이 조건과 관련된 락이 있어야한다.
pthread_cond_wait()은 호출하는 스레드를 대기상태로 만들어 다른 스레드가 신호를 줄 때까지 기다린다. 일반적으로 다음과 같이 사용한다.
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
Pthread_mutex_lock(&lock);
while (ready == 0)
Pthread_cond_wait(&cond, &lock);
Pthread_mutex_unlock(&lock);
여기서 ready가 0이 아니면 스레드는 ready상태에 빠진다.
다른 스레드에서 실행되는 스레드를 깨우려면,
Pthread_mutex_lock(&lock);
ready = 1;
Pthread_cond_signal(&cond);
Pthread_mutex_unlock(&lock);
여기서 몇가지 주목할 점이 있다.
첫 번째, 신호를 보내거나 ready를 수정할 때 락이 있는지 확인함으로써 race condition을 피한다.
두 번째, wait호출은 매개변수에 락이 있지만 signal호출은 조건만 사용한다. 이것은 wait호출이 호출하는 스레드를 ready로 만들때 락을 해제하기 때문이다. 그래서 다른 스레드가 락을 획득하고 깨워줄 수 있는 것이다. 깨어난 후에는 락을 재 획득한다.
세 번째, 대기 스레드는 if문이 아니라 while 루프에서 조건을 다시 확인한다. 이게 안전하기 때문이다.