피터슨의 해결안은 이론적으로는 프로세스 동기화를 위한 3가지 조건을 만족하지만 연산의 하드웨어 구현상의 문제로 인해서 스레드들이 임계구역으로 접근하기 위한 권한을 얻는 과정에서 경합상황이 발생하게 되었다.
이런 구현상의 한계는 운영체제를 디자인 할 때 부터 "임계구역으로 접근하는 연산"의 경우 원자성을 보장해주도록 하면 된다. 그 중하나가 뮤텍스(mutex)이다.
우선 뮤텍스의 기본적인 개념은 피터슨의 해결안과 크게 다르지 않다.
#define TRUE (1)
#define FALSE (0)
static pthread_mutex_t mutex; // [0]
static int s_num = 0;
void* test2(void* p)
{
int i;
pthread_mutex_lock(&mutex); // [1]
for (i = 0; i < 10000000; ++i) {
++s_num;
}
pthread_mutex_unlock(&mutex); // [2]
return NULL;
}
int main(void)
{
pthread_t thread1;
pthread_t thread2;
pthread_t thread3;
pthread_mutex_init(&mutex, NULL); // [3]
pthread_create(&thread1, NULL, test2, NULL);
pthread_create(&thread2, NULL, test2, NULL);
pthread_create(&thread3, NULL, test2, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_join(thread3, NULL);
printf("result : %d\n", s_num);
return 0;
}
[0] 우선 뮤텍스 변수를 선언한다. 이 변수를 통해서 임계구역으로 접근하는 권한(?)을 얻게 된다.
뮤텍스를 선언했다면 해당 뮤텍스를 [3]과 같이 초기화 해주어야 한다. 이제 임계구역에 있는 s_num 변수에 연산을 가하는 thread1에서 임계구역으로 접근하기 전에 mutex변수 lock해야한다. 말그대로 임계구역에 들어가고 나서 다른 스레드가 못들어오도록 접근을 잠근다(lock)는 뜻이다.
반복문을 실행하는 횟수가 충분히 커도 예상된 결과 값이 나온것을 확인할 수 있다.
임계구역으로 접근하기 위해 mutex
변수에 대한 연산을 해주는 pthread_mutex_lock
에서 이루어지는 과정은 대략 아래와 같다고 볼수 있다.
- mutex 를 획득하기 위해서 스레드간 경합한다
- mutex를 획득했다면 임계구역 내에 다른 스레드의 접근을 막기 위해 mutex를 lock으로 변경한다.
위의 두가지 연산은 하드웨어에 의해서 원자성이 보장된다. 즉, 타임쉐어링 때문에 스케줄러에 의한 문맥교환(context switching)이 일어나지 않는다는 뜻이다.
여기서 1에 주목할 필요가 있다.
만약 thread1이 [1]을 지나서 임계구역에 들어가서 연산을 하고 있다면 thread2와 thread3는 [1]에서 반복문을 돌면서 계속 mutex가 unlock되어 있는지 확인하는 바쁜대기(busy waiting) 상태에 빠진다. 이런 의미없는 반복문의 실행 때문에 cpu를 점유하고 있어서 다른 프로세스가 cpu를 할당받을 기회를 줄인다.
하지만 cpu가 멀티코어인 경우에는 상황이 달라진다.
thread2와 thread3이 바쁜대기를 하지 않는다면 대략 다음과 같은 과정으로 thread2와 thread3이 cpu를 할당받을 것이다.(다음에 배울 세마포어 플로우와 유사하기 때문에 이해를 위해서 참고만 하도록 한다)
- thread2는 mutex가 lock된것을 확인하고 자기자신을 sleep상태로 만들어서 wait queue에 넣는다.
- thread1이 끝나고 mutex가 unlock 되는 과정에서 wait queue에 있는 것을 ready queue로 끄집어 온다.
- 스케줄러의 ready queue 선택을 받아서 다음 스레드가 실행된다.
바쁜대기(busy waiting)을 하지 않는다면 위와같은 과정이 발생해서 뮤텍스가 release 될 때 까지 wait queue에서 기다리고 다시 실행되기 까지 두번의 문맥교환(context switching)을 겪게 된다.
이런 이유때문에 cpu가 멀티코어라면 thread1이 하나의 코어에서 임계구역에 대한 연산을 하고 있고, thread2는 다른 코어에서 계속 반복문을 돌면서 thread1이 임계구역에서 빠져나올 때 까지 기다리기 때문에 문맥 교환(context switching)없이 바로 mutex를 획득해서 임계구역으로 들어갈 수 있다.
이렇게 mutex lock을 사용할 수 있을 때 까지 스레드가 계속 회전하는 것을 스핀락(spin lock)이라고 한다
뮤텍스는 프로세스 동기화의 문제 3가지(상호배제, 진행, 한정대기)중에서 상호배제에 관한 문제만을 해결한다. "진행"에 해당하는 데드락(dead lock) 문제와, "한정대기"에 해당하는 기아(starvation) 문제에 대해서는 해결책을 제시하지 못한다.
그리고 스핀락(spin lock)을 통해서 문맥교환 없이 mutex lock을 사용할 수 있지만, 바쁜대기(busy waiting)을 통해서 의미없이 cpu를 차지하고 있는게 항상 효율적인 것은 아니다.
따라서 다음시간에는 위에서 잠깐 살펴본 mutex lock이 사용중일 때 대기 스레드를 wait queue에 넣는 세마포어(semaphore)에 대해서 알아보도록 하자