뮤텍스를 통한 프로세스 동기화에서 멀티코어 cpu인 경우 스핀락을 통해서 문맥교환 없이 스레드들은 뮤텍스 락을 얻기 위해 경합하고 임계구역에 접근하는 방법을 알아보았다.
하지만 cpu를 획득 하기 위해서 끊임없이 반복문을 실행하고 있는 바쁜대기(busy waiting)
상태로 인해서 다른 프로세스들이 cpu를 할당받을 기회를 줄이는 것도 사실이다.
이번에는 프로세스가 대기상태일때 바쁜대기가 없도록 구현된 세마포어에 대해서 알아보자
세모포어에 대해서는 운영체제 책(공룡책으로 널리 알려진)에서 나오는 설명들을 다루어 보겠다.
typedef struct {
int value;
struct process* list;
} semaphore;
semaphore를 관리하는 구조체가 있으며 value는 공유자원으로 들어갈 수 있는 열쇄(?) 개수를 의미한다.
list는 현재 대기상태에 있는 스레드의 목록이다.(커널 레벨에서는 list안에 들어있는 모든 스레드는 wait queue에 들어가 있을 것이다)
세마포어에서 임계구역(critical section)으로의 접근을 위한 경합은 다음과 같이 이루어진다
thread1이 임계구역(critical section)에서 연산을 하고 있다고 가정해보자.
wait(semaphore* s)
{
s->value--;
if (s->value < 0) {
/* add this thread to s->list */
sleep();
}
}
thread2는 임계구역(critical section)으로의 접근을 위해서 우선 wait() 상태에 빠진다. 여기서 s->value가 0보다 작으면 임계구역으로 접근하기 위한 열쇄가 없다는 뜻이기 때문에 자기자신을 대기 스레드 목록인 s->list에 넣는다.그리고 sleep() 함수를 통해서 자기자신의 스레드 상태를 wait으로 바꾸고 wait queue에 넣는다.
thread1로 다시 돌아와서, 임계구역(critical section)에 대한 연산을 끝마치면 signal() 함수를 호출한다.
signal(semaphore* s)
{
s->value++; /* [1] */
if (s->value <= 0) {
/* remove this process P from s->list */
wakeup(P)
}
}
signal 함수에서는 방금 임계구역(critical section)에 대한 연산을 끝마쳤으니 [1]을 통해서 열쇄를 반납한다. 열쇄를 반납해서 value가 1이상이 되면 더이상 대기중인 스레드가 없다는 뜻이기 때문에 실행을 하지 않고, 0이하일때만 s->list에 있는 wait상태의 스레드를 ready queue로 집어넣어서 cpu를 할당받을 수 있도록 한다.
#include <semaphore.h>
#define TRUE (1)
#define FALSE (0)
static sem_t sem;
static int s_num = 0;
void* test3(void* p)
{
int i;
for (i = 0; i < 10000000; ++i) {
sem_wait(&sem); /* [2] */
++s_num;
sem_post(&sem); /* [3] */
}
return NULL;
}
int main(void)
{
pthread_t thread1;
pthread_t thread2;
pthread_t thread3;
sem_init(&sem, 0, 1); /* [1] */
pthread_create(&thread1, NULL, test3, NULL);
pthread_create(&thread2, NULL, test3, NULL);
pthread_create(&thread3, NULL, test3, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_join(thread3, NULL);
printf("result : %d\n", s_num);
return 0;
}
세마포어도 뮤텍스와 마찬가지로 [1]에서 처럼 초기화를 해주어야 한다. 여기서 세번째 인자의 경우 임계구역으로 접근할수 있는 열쇄의 개수인데 이게 1개이면 뮤텍스와 동작이 동일하다.
그리고 [2] pthread_mutex_lock
에 해당하는 sem_wait
를 통해서 스레드가 임계구역으로 접근할때 처리를 해준다. 임계구역에 대한 연산이 끝났다면 [3] pthread_mutex_unlock
에 해당하는 sem_post
를 해줘서 대기중인 스레드를 wakeup 시켜준다.
만약 임계구역으로 접근가능한 스레드의 개수를 초기화 단계
sem_init(&sem, 0, 10);
에서 10개로 늘리면 어떻게 될까?
임계구역으로 접근한 10개의 스레드가 공유자원에 대한 경합상태에 빠져서 연산의 결과를 보장할 수 없게 된다. 이런 이유때문에 스레드 동기화를 의도한다면 초기화를 할때는 뮤텍스와 동일하게 동작하는 binary semaphore를 사용하기 위해서 1로 설정하도록 하자.
뮤텍스가 스핀락(spin lock)을 통해서 대기중인 스레드를 바쁜대기(busy waiting) 상태로 관리하는 반면 세마포어는 임계구역이 사용중이면 대기중인 스레드의 상태를 wait으로 바꾸고 wait queue에 넣어버린다.
뮤텍스는 (멀티코어 기준)임계구역에 접근할 때 문맥교환을 최소화 해서 접근할 수 있는 반면 세마포어는 wait queue에 잇는 스레드를 wakeup시켜서 ready queue로 넣고, ready queue에 있는 스레드가 스케줄러에 의해서 cpu를 배정받을 때 까지 기다려야 한다.