상호 배제
상호 배제(mutual exclusion, Mutex, 뮤텍스)는 동시 프로그래밍에서 공유 불가능한 자원의 동시 사용을 피하기 위해 사용되는 알고리즘으로, 임계 구역(critical section)으로 불리는 코드 영역에 의해 구현된다.공유 불가능한 자원의 예로는 동시에 실행되고 있는 프로그램간의 통신에 사용되는 비트 단위의 깃발, 계수기, 큐 등이다. 문제는 스레드가 언제라도 정지되거나 시작될 수 있다는 것이다.예) 프로그램의 일부분이 여러 단계를 거치면서 데이터를 읽고 쓰고 있다고 하자. 그런데 예상치 못한 사건 등에 의해 다른 스레드가 동작하기 시작했다. 첫 번째의 스레드가 쓰고 있는 영역에서, 이 두 번째의 스레드가 또 다른 작업을 시작한다면, 해당 영역의 값은 부적절하며 예상할 수 없는 상태에 놓이게 된다. 게다가 두 번째의 스레드가 값을 덮어 써버리기라도 한다면, 복구 불가능한 상태로 되고 만다. 그러므로 공유 데이터를 접근하는 프로그램 내부의 이른바 임계 구역이라는 부분은 홀로 수행되도록 보호되어야 하며, 다른 스레드가 동일한 부분의 프로그램을 수행해서 동일한 공유 데이터를 접근하는 것을 막아야 한다. 단일 프로세서 시스템에서, 상호 배제를 구현하는 가장 단순한 방법은 인터럽트를 억제해서 공유 데이터가 손상되는 것을 막는 것이다. 성능에 최소한의 영향을 주기 위해 인터럽트가 발생하지 않을 명령어 집합의 수는 가능한 최소로 유지시키는 것이 좋다.
쓰레드에서의 데이터 동기화 문제
예를 들어서 두개의 쓰레드가 2개의 변수를 업데이트하려고 하는데, 하나의 쓰레드는 2개의 변수를 0으로 다른 하나는 1로 변경을 시도한다고 하자. 만약 이러한 변경이 같은 시간(same time)에 일어난다면 하나는 0으로 변경이 되었지만 다른 하나는 1로 변경되는 문제가 발생할수도 있을것이다. 이러한 경우 충돌이 발생할 수 있을 것이다. 하나의 쓰레드가 변경하고자 하는 첫번째 변수를 0으로 바꾸었는데, 2번째 변수를 0으로 바꾸기 전에 다른 쓰레드가 변수를 모두 1로 바꾸어 버릴수 있기 때문이다. 그리고 나서 다시 첫번째 쓰레드가 두번째 변수를 0으로 바꾸게 되면, 첫번째 변수는 1이지만 두번째 변수는 0이 되는 데이타 불일치 상황이 발생하게 될것이다.
int a = 1;
Thread A 에서 a를 읽어들인다.
Thread B 에서 a를 읽어들인다.
Thread A 에서 a = a+1를 한다.
{
a = a+1;
결과는 2;
}
Thread B 에서 a++를 한다.
{
a = a + 1; // 읽어들인 값이 1이기 때문에
역시 결과는 2가 된다.
}
두번의 a+1 과정을 거쳤음으로, a가 3이 되는걸 기대하겠지만 이러한 류의 동시접근 문제로 a는 2가 되어버리고 만다.
Mutex를 이용하면 된다.
pthread 라이브러리는 이러한 문제의 해결을 위해서 mutex 라는 것을 사용한다. mutex 는 IPC에서 데이타 접근 통제를 위해서 사용하는 세마포어와 매우 비슷한 일을한다. mutex 는 세마포어와 마찬가지로 잠금 을 사용하는데, mutex 는 lock 에 대해서 다음과 같은 3가지 사항에 대해서 보증한다.
* Atomicity - mutex 잠금(lock)는 최소단위 연적(atomic operation) 으로 작동한다. 이말의 뜻은 하나의 쓰레드가 mutex 를 이용해서 잠금을 시도하는 도중에 다른 쓰레드가 mutex 잠금을 할수없도록 해준다는 뜻이다. 한번에 하나의 mutex 잠금을 하도록 보증해준다.
* Singularity - 만약 스레드가 mutex 잠금을 했다면, 잠금을 한 쓰레드가 mutex 잠금을 해제 하기 전까지 다른 어떠한 쓰레드도 mutex 잠금을 할수 없도록 보증해준다.
* Non-Busy Wait - 바쁜대기 상태에 놓이지 않는다는 뜻으로, 하나의 쓰레드가 mutex 잠금을 시도하는데 이미 다른 쓰레드가 mutex 잠금을 사용하고 있다면 이쓰레드는 다른 쓰레드가 락을 해제하기전까지 해당 지점에 머물러 있으며 이동안은 어떠한 CPU 자원도 소비하지 않는다 (이를테면 sleep).
mutex 는 위의 3가지 사항을 보증해 줌으로 우리는 쓰레드 공유되는 메모리를 충돌없이 사용할수 있게 된다. mutex 를 통해서 잠금을 얻는 것을 간단하게 나타내면 아래와 같다(pseude-code 형식으로 설명하겠다). 일단 첫번째 쓰레드는 다음과 같이 mutex 를 이용해서 변수 업데이트 작업을 하게 될것이다.
'X1' 에 대해서 뮤텍스 잠금을 한다.
첫번째 변수를 0으로 세팅한다.
두번째 변수를 0으로 세팅한다.
'X1' 뮤택스의 잠금을 해제한다.
그리고 나서 2번째 쓰레드가 뮤텍스를 이용한 변수 업데이트 작업을 하게 될것이다.
'X1' 에 대해서 뮤텍스 잠금을 한다.
첫번째 변수를 1으로 세팅한다.
두번째 변수를 1으로 세팅한다.
'X1' 뮤택스의 잠금을 해제한다.
2개의 쓰레드가 같은 mutex 'X1' 을 사용한다고 가정하면 이 코드는 어떠한상황 에서도 두개의 변수를 '0' 혹은 '1' 로 유지할것이라는걸 보증할수 있을것이다. mutex 를 이용해서 이러한 작업을 할때 프로그래머는 어떠한 상황에서 mutex 를 써야할지에 대해서 주의를 기울여야 한다.
만약 3번째 쓰레드가 mutex 를 사용하지 않은체 변수에 업데이트 작업을 하게 될경우 앞의 2개의 쓰레드가 mutex 를 사용한 이유가 사라지게 된다.
뮤텍스 생성과 초기화
뮤텍스를 생성하기 위해서 우리는 먼저, 뮤텍스정보를 저장하기 위한 타입인 pthread_mutex_t 를 선언해주고 이것을 초기화 해주어야 한다. 선언과 초기화의 가장간단한 방법은 PTHREAD_MUTEX_INITIALIZER 상수를 할당하는 것으로 아래와 같이 사용할수 있다.
pthread_mutex_t a_mutex = PTHREAD_MUTEX_INITIALIZER;
뮤텍스 잠금, 잠금해제, 제거
뮤텍스 잠금을 위한 함수로는 pthread_mutex_lock() 함수를 제공한다. 이 함수는 해당 뮤텍스에 대해서 잠금을 시도하는데, 만약 잠그려는 뮤텍스가 다른 쓰레드에 의해서 이미 잠겨있다면, 잠금을 얻을수 있을때까지 - 이미 잠근 다른 쓰레드가 뮤텍스의 잠금을 해제할때까지 - 봉쇄(블럭)되게 된다. 다음은 이러한 뮤텍스 잠금을 얻기 위한 지원함수들이다.
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_destory(pthread_mutex_t *mutex);
pthread_mutex_trylock() 를 사용하면 잠금을 얻을수 없을경우 해당 코드에서 블럭되지 않고 바로 에러코드를 돌려준다. 즉 pthread_mutex_lock 의 비봉쇄 버젼이라고 생각하면 된다.
뮤텍스 잠금을 얻은후 해당 영역에서의 작업을 마친후 잠금을 해제하기 위해서 사용한다. 사용되는 함수는 pthread_mutex_unlock(3) 이며 함수원형은 다음과 같다.
int pthread_mutex_unlock(pthread_mutex_t *mutex);
다음은 쓰레드간 공유되는 자원을 위해서 잠금을 어떻게 사용하는지를 보여주는 간단한 예제다.
예제: mutex_lock.c
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int ncount; // 쓰레드간 공유되는 자원
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 쓰레드 초기화
void* do_loop(void *data)
{
int i;
pthread_mutex_lock(&mutex); // 잠금을 생성한다.
for (i = 0; i < 10; i++)
{
printf("loop1 : %d", ncount);
ncount ++;
sleep(1);
}
pthread_mutex_unlock(&mutex); // 잠금을 해제한다.
}
void* do_loop2(void *data)
{
int i;
// 잠금을 얻으려고 하지만 do_loop 에서 이미 잠금을
// 얻었음으로 잠금이 해제될때까지 기다린다.
pthread_mutex_lock(&mutex); // 잠금을 생성한다.
for (i = 0; i < 10; i++)
{
printf("loop2 : %d", ncount);
ncount ++;
sleep(1);
}
pthread_mutex_unlock(&mutex); // 잠금을 해제한다.
}
int main()
{
int thr_id;
pthread_t p_thread[2];
int status;
int a = 1;
ncount = 0;
thr_id = pthread_create(&p_thread[0], NULL, do_loop, (void *)&a);
sleep(1);
thr_id = pthread_create(&p_thread[1], NULL, do_loop2, (void *)&a);
pthread_join(p_thread[0], (void *) &status);
pthread_join(p_thread[1], (void *) &status);
status = pthread_mutex_destroy(&mutex);
printf("code = %d", status);
printf("programing is end");
return 0;
}
위의 코드를 우선 mutex 잠금을 하지 않은체 컴파일후 실행해보자. 간단하게 pthread_mutext_lock 와 pthread_mutex_unlock 부만 주석처리하면 된다. 그러면 do_loop2 와 do_loop 가 일정한 간격을 두고 ncount 자원에 접근하는 것을 볼수 있을것이다. 그러나 우리는 do_loop 가 ncount 자원을 접근하고 있는동안 다른 쓰레드가 접근하지 않기를 원할때가 있을것이다. 이럴때 뮤텍스 잠금을 사용하면 된다.
위의 코드에서 뮤텍스 잠금 부분의 주석을 풀고 다시 컴파일해서 실행시켜보면, do_loop 쓰레드가 ncount 증가 작업을 모두 마칠때까지 do_loop2 쓰레드는 해당 영역에서 블럭됨을 알수 있을것이다. 이런식으로 하나의 쓰레드가 특정자원에 접근할때 다른 쓰레드가 접근하지 못하도록(한번에 하나의 쓰레드만 해당 자원에 접근할수 있도록) 제어할수 있다.
컴파일 방법은 gcc -o mutex_lock mutex_lock.c -lpthread 이다
더이상 뮤텍스를 사용할일이 없다면 pthread_mutex_destory 를 이용해서 뮤텍스 자원을 제거(free) 하도록 한다. 만일 뮤텍스자원을 사용하는 쓰레드가 하나라도 존재한다면 에러코드(EBUSY)를 리턴한다. 그러므로 모든 쓰레드의 뮤텍스에 대해서 pthread_mutex_unlock 을 이용해서 잠겨져야만 뮤텍스 제거가 성공할수 있다. 성공할경우 0을 넘겨준다.
'백엔드기술 > 개발언어' 카테고리의 다른 글
임시 참조 (0) | 2010.05.25 |
---|---|
프로세스와 스레드 (0) | 2009.12.09 |
Shuffle Array (0) | 2009.12.09 |