앞선 포스팅 - 락(lock)의 필요성과 구현 마지막 부분에서 컨디션 변수의 개념을 약간 이나마 다뤘다. 여기에서 조금 더 자세하게 살펴본다. 세마포어를 알아보고, 세마포어로 락과 조건 변수를 만드는 게 가능한지와 락과 커디션 변수를 사용하여 세마포어처럼 사용할 수 있는지를 알아본다.
while (done == 0) ;
int done = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;
void thr_exit() {
pthread_mutex_lock(&m);
done = 1;
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}
void *child(void *arg) {
printf("child\n");
thr_exit();
return NULL;
}
void thr_join() {
pthread_mutex_lock(&m);
while (done == 0)
pthread_cond_wait(&c, &m);
pthread_mutex_unlock(&m);
}
int main(int agrc, char *argv[]) {
printf("parent: begin\n");
pthread_t p;
pthread_create(&p, NULL, child, NULL);
thr_join();
printf("parent: end\n");
return 0;
}
void thr_exit() {
pthread_mutex_lock(&m);
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}
void thr_join() {
pthread_mutex_lock(&m);
pthread_mutex_unlock(&m);
}
void thr_exit() {
done = 1;
pthread_cond_signal(&c);
}
void thr_join() {
if (done == 0)
pthread_cond_wait(&c);
}
if (done == 0)
pthread_cond_wait(&c, &m);
세마포어는 추상 자료형(Abstract Data Type, ADT)으로 사용할 수 있는 자원의 개수를 정수형으로 가지고, P(S)와 V(S) 연산을 가진다. POSIX 표준에서는 sem_wait()와 sem_post()를 제공한다.
세마포어는 초깃값에 그 동작이 결정되기 때문에 사용하기 전에 먼저 초기화해야 한다.
처음에 세마포어를 접할 때 헷갈릴 수 있는 점은 경쟁 조건을 예방하기 위해 공유자원에 배타적인 접근을 가능하게 하는 것인데, 세마포어의 개수만큼 공유자원에 접근하면 데이터 무결성이 해치지 않을까 생각할 수 있다. 상호 배제가 필요한 경우 mutex나 이진 세마포어를 사용하지만, 안전한 작업을 병렬로 처리하고 싶을 때(그저 데이터 접근하여 읽는 것)는 카운팅 세마포어를 사용하여 성능을 개선할 수 있는 것이다.
sem_t s;
sem_init(&s, 0, 1);
int sem_wait(sem_t *s) {
세마포어의 값을 하나 감소시킨다.
세마포어의 값이 음수라면 기다린다.
}
int sem_post(sem_t *s) {
세마포어의 값을 하나 증가시킨다.
하나 이상의 쓰레드가 기다리고 있다면, 하나를 깨운다.
}
sem_t m;
sem_init(&m, 0, 1);
sem_wait(&m);
// 임계영역
sem_post(&m);
이진 세마포어와 뮤택스는 무슨 차이가 있을까?
- 뮤택스의 경우 락을 획득한 쓰레드만이 락을 해제할 수 있다. 하지만, 세마포어의 경우 자원 사용이 완료되면 해당 자원에 접근했던 쓰레드가 아니더라도 신호를 보낼 수 있다. A쓰레드가 특정 이벤트가 생기기 전까지 기다리고(sem_wait), 관리자 쓰레드가 이벤트가 발생했을 때 특정 쓰레드를 깨울 수 있기 때문이다(sem_post).
sem_t s;
void *child(void *arg) {
printf("child\n");
sem_post(&s); // 시그널 전달: 자식의 동작이 끝남
return NULL;
}
int main(int argc, char *argv[]) {
sem_init(&s, 0, X); // X의 값은?
pthread_t c;
pthread_create(c, NULL, child, NULL);
sem_wait(&s);
return 0;
}
이를 위해 초기 세마포어 값을 어떻게 설정해야 할까?
0으로 설정한다.
1의 경우 자식 프로세스가 sem_post()을 실행하기 전에 부모가 sem_wait()을 호출한다. 부모 프로세스는 자식이 실행될 때까지 대기해야 한다. 따라서 wait()호출 전에 세마포어 값이 0보다 같거나 작아야 한다. 즉, 초기 세마포어 값은 0이다.
2의 경우 자식이 먼저 sem_post()를 호출하여 세마포어의 값을 0에서 1로 증가시킨다. 부모 프로세스가 sem_wait()를 호출하면 세마포어 값이 1인 것을 발견하고 이를 0으로 만들어 sem_wait()를 대기 없이 리턴한다.
typedef struct __Zem_t {
int value;
pthread_cond_t cond;
pthread_mutex_t lock;
} Zem_t;
void zem_init(zem_t *s, int value) {
s->value = value;
cond_init(&s->cond);
mutex_init(&s->lock);
}
void zem_wait(zem_t *s) {
mutex_lock(&s->lock);
while (s->value <= 0)
cond_wait(&s->cond, &s->lock);
s->value--;
mutex_unlock(&s->lock);
}
void zem_post(zem_t *s) {
mutex_lock(&s->lock);
s->value++;
cond_signal(&s->cond);
mutex_unlock(&s->lock);
}
int buffer[MAX};
int fill = 0;
int use = 0;
void put(int value) {
buffer[fill] = value;
fill = (fill + 1) % MAX;
}
int get() {
int tmp = buffer[use];
use = (use + 1) % MAX;
return tmp;
}
sem_t empty;
sem_t full;
void *producer(void *arg) {
int i;
for (int i = 0; i < loops; i++) {
sem_wait(&empty);
put(i);
sem_post(&full);
}
}
void *consumer(void *arg) {
int tmp = 0;
while (tmp != -1) {
sem_wait(&full);
tmp = get();
sem_post(&empty);
}
}
int main(void) {
// ...
sem_init(&empty, 0, MAX); // MAX 버퍼는 비어 있는 상태로 시작
sem_init(&full, 0, 0); // 0이 가득 차 있는 상태
}
MAX의 값이 1보다 크고, 또한 생산자와 소비자 쓰레드들이 있다고 하면 경쟁 조건이 발생한다.
두 생산자가 put()을 거의 동시에 호출한다면 값이 덮어씌워지게 된다. 상호 배제를 추가한다.
void *producer(void *arg) {
int i;
for (int i = 0; i < loops; i++) {
sem_wait(&mutex);
sem_wait(&empty);
put(i);
sem_post(&full);
sem_wait(&mutex);
}
}
void *consumer(void *arg) {
int tmp = 0;
while (tmp != -1) {
sem_wait(&mutex);
sem_wait(&full);
tmp = get();
sem_post(&empty);
sem_wait(&mutex);
}
}
상호 배제는 해결한 거 같지만, 이번에는 교착 상태가 발생한다. 어디에서 발생할까?
소비자가 먼저 실행이 되었다. full값이 0이므로 -1로 바꾸고, 소비자는 대기한다. mutex를 가진 채로 말이다. 생산자는 mutex를 얻을 수 없다.
void *producer(void *arg) {
int i;
for (int i = 0; i < loops; i++) {
sem_wait(&empty);
sem_wait(&mutex);
put(i);
sem_wait(&mutex);
sem_post(&full);
}
}
void *consumer(void *arg) {
int tmp = 0;
while (tmp != -1) {
sem_wait(&full);
sem_wait(&mutex);
tmp = get();
sem_wait(&mutex);
sem_post(&empty);
}
}
typedef struct _rwlock_t {
sem_t lock; // 이진 세마포어(기본 락)
sem_t writelock; // 하나의 쓰기 또는 다수의 읽기 락을 위한 락
int readers; // 임계 여역 내 읽기를 수행 중인 reader 수
} rw_lock_t;
void rwlock_init(rw_lock_t *rw) {
rw->readers = 0;
sem_init(&rw->lock, 0, 1);
sem_init(&rw->writelock, 0, 1);
}
void rwlock_acquire_readlock(rwlock_t *rw) {
sem_wait(&rw->lock);
rw->reader++;
if (rw->reader == 1)
sem_wait(&rw_writelock); // 읽기용 락이 writelock을 획득
sem_post(&rw->lock);
}
void rwlock_release_readlock(rwlock_t *rw) {
sem_wait(&rw->lock);
rw->readers--;
if (rw->readers == 0)
sem_post(&rw->writelock); // 마지막으로 읽기용 락이 wirtelock 해제
sem_post(&rw->lock);
}
void rwlock_acquire_writelock(rwlock_t *rw) {
sem_wait(&rw->writelock);
}
void rwlock_release_writelock(rwlock_t *rw) {
sem_post(&rw->writelock);
}
whjie (1) {
think();
getforks();
eat();
putforks();
}
int left(int p) { return p;}
int right(int p) { return (p + 1) % 5; }
void getforks() {
sem_wait(forks[left(p)]);
sem_wait(forks[right(p)]);
}
void getforks() {
sem_post(forks[left(p)]);
sem_post(forks[right(p)]);
}