메뉴 건너뛰기


Developer > Application
4장. 뮤텍스(Mutex)
 
뮤텍스는 Mutual Exclusion의 줄임말입니다. 굳이 해석하면 "상호배제"가 되겠네요.
해석의 의미는 모호하지만 뮤텍스 객체를 두 쓰레드가 동시에 사용할 수 없다는 것입니다.
앞장의 크리티컬 섹션을 이해하셨다면 이미 뮤텍스의 방법 역시 이해하신 겁니다.
다들 개념이 비슷비슷하거든요. 굳이 다시 설명한다면 하나뿐인 화장실에 한 사람이 들어가 문을 잠그고 볼일을 보는 동안 다른 사람은 그 사람이 문을 열어 줄 때까지 기다려야 한다. 입니다.
 
윈도우즈와 유닉스에서의 뮤텍스 사용법이 좀 다른데 우선 유닉스를 보겠습니다.
 
 
1. 유닉스/리눅스에서의 뮤텍스
 
 
유닉스에서 뮤텍스 관련함수는 다음과 같은 것들이 있습니다.
 
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
 
이미 짐작하셨게지만 pthread_mutex_init()함수로 뮤텍스를 생성하고 초기화를 합니다. 뮤텍스의 사용이 모두 끝나면 pthread_mutex_destroy()함수를 호출합니다.
뮤텍스를 사용할 때는 pthread_mutex_lock()과 pthread_mutex_unlock()함수로 락을 걸었다 풀었다 하면 됩니다. 뮤텍스를 검사할때는 pthread_mutex_trylock()함수를 사용하면 됩니다.
 
뮤텍스를 생성하고 초기화하는 방법은 정적인 방법과 동적인 방법 두가지가 있습니다.
 
정적인 방법은 다음과 같이 해서 생성 및 초기화를 합니다.
 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
 
만약 프로그램 실행 중 동적으로 뮤텍스를 생성하려면 pthread_mutex_init()함수를 사용합니다. 다시 말해서 뮤텍스를 사용한다고 해서 반듯이 pthread_mutex_init()함수를 사용해야 하는 것은 아니라는 것이죠.
pthread_mutex_init()함수의 두번째 아큐먼트인 mutexattr은 뮤텍스의 속성을 지정한다고 되어 있는데... 자세한 설명은 어디를 봐도 찾기가 힘드네요. 일반적으로 NULL을 사용합니다. 기본 뮤텍스를 만든다고 하는데... 다른 값을 사용한 예를 보질 못해서... 그냥 우리도 NULL만을 사용하기로 하죠.
 
모든 함수는 함수의 역할을 성공하면 0을 리턴합니다. 그외의 값은 에러입니다.
pthread_mutex_lock()함수는 해당 뮤텍스가 이미 lock되어 있는 경우 블로킹됩니다.
pthread_mutex_trylock()함수는 해당 뮤텍스를 사용할 수 있을 때는 락을 걸고 0을 리턴합니다. 이미 락이 되어 있는 경우는 EBUSY값을 리턴하고 빠져 나옵니다.  
 
#include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> int cnt=0; pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; void *Thread1(void *arg) { int i, tmp; for(i=0; i<1000; i++){ pthread_mutex_lock(&mutex); tmp=cnt; usleep(1000); cnt=tmp+1; pthread_mutex_unlock(&mutex); } printf("Thread1 End\n"); return NULL; } void *Thread2(void *arg) { int i, tmp; for(i=0; i<1000; i++){ pthread_mutex_lock(&mutex); tmp=cnt; usleep(1000); cnt=tmp+1; pthread_mutex_unlock(&mutex); } printf("Thread2 End\n"); return NULL; } int main(int argc, char *argv[]) { pthread_t thread1; pthread_t thread2; pthread_create(&thread1, NULL, Thread1, NULL); pthread_create(&thread2, NULL, Thread2, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("%d\n", cnt); return 0; }
 
앞장에서 보였던 샘플과 동일하니깐... 이해하기는 쉽겠죠? mutex관련 코드를 주석처리하면 결과 값이 1000이 나오게 되구요. mutex관련 코드를 살리면 2000이 최종 결과로 나오게 됩니다.
사용법도 크리티컬 섹션과 뭐 다른 것도 없죠?
 
 
2. 윈도우즈에서의 뮤텍스
 
윈도우즈에서 제공하는 뮤텍스 함수는 유닉스의 것과 사용 방법이 좀 다릅니다.
유닉스에서의 뮤텍스는 하나의 프로세스 안에서의 여러 쓰레드간의 동기화를 담하는 것에 반해 윈도우즈에서는 서로 다른 프로세스 간의 동기화도 가능합니다.
 
우선 뮤텍스를 이야기 하기 전에 윈도우즈에서 커널 모드의 동기화 객체를 대기하는 함수를 알아보도록 하겠습니다.
 
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE *lpHandles, BOOL fWatAll, DWORD dwMilliseconds);
 
동기화를 위해 객체의 사용을 대기하는 함수는 두가지가 있습니다. WaitForSingleObject()함수는 하나의 커널 객체를 기다리게 되고 WaitForMultipleObjects()함수는 이름에서 알 수 있듯이 여러 커널 객체를 기다릴 수 있습니다.
여기서 말하는 커널 객체는 뮤텍스, 세마포어, 쓰레드, 프로세스, 이벤트, 파이프 등이 이에 해당됩니다.
커널 객체는 두가지 상태를 가집니다. 하나는 신호상태(Signaled)이고 또 다른 하나는 비신호상태(Nonsignaled)입니다.
여기서 신호상태란 신호등에 파란불이 들어온 것과 같은 상태로 해당 객체를 사용할 수 있는 상태를 이야기 합니다. 비신호상태란 신호등에 빨간불이 들어온 것과 같이 해당 객체를 다른 쓰레드가 사용하고 있으니 기다리라는 상태입니다.
위에서 이야기한 두개의 함수는 지정한 객체가 신호상태일 때까지 기다리는 대기 함수입니다. 정확히 이야기 하면 신호상태의 객체를 기다려서 신호상태의 객체를 받으면 바로 그 객체를 비신호상태로 바꾸는 함수입니다.
 
그럼 함수 하나씩 이야기 해보죠.
먼저 WaitForSingleObject()함수입니다. 하나의 커널 객체가 신호상태가 될때까지 기다리는 함수로 다음과 같은 아큐먼트를 사용합니다.
- hHandle: 대상이 되는 커널 객체의 핸들입니다.
- dwMilliseconds: 최대 언제까지 기다릴지를 밀리초로 지정합니다. 이런걸 보고 타임아웃이라고 하죠? 즉, 신호상태가 될때까지 대기하긴 하는데 교착상태(Dead Lock)를 피하기 위해 최대 기다릴 시간을 지정해 주는 것이죠.
즉, WaitForSingleObject()함수가 대기를 마치고 리턴 되는 경우는 두가지 경우가 있겠죠? 하나는 해당 객체가 신호상태가 된 것이고 또 다른 하나는 타임 아웃 시간이 경과한 경우입니다.
 
리턴되는 값으로는 다음의 세가지 값 중에 하나입니다.
- WAIT_OBJECT_0 : hHandle객체가 신호상태로 되어 리턴함
- WAIT_TIMEOUT : 지정한 타임 아웃 시간이 모두 경과하여 리턴함
- WAIT_ABANDONED : hHandle객체가 신호상태로 되긴 했는데 hHandle객체를 잡고 있는 쓰레드가 해당 객체를 포기했을 경우에 리턴함
 
이번에는 WaitForMultipleObjects()함수입니다. 하나의 객체가 아니라 여러객체를 다룬 다는 점만 빼고는 WaitForSingleObject()와 같습니다. 여러 객체를 지정해야 함으로 사용방법이 좀 복잡합니다. 아큐먼트는 다음과 같습니다.
 
- nCount : 감시하고자 하는 커널 객체의 수를 지정합니다.
- lpHandles: 감시하고자 하는 커널 객체를 배열로 전달합니다. 이 배열의 크기가 nCount가 됩니다.
- fWaitAll : 감시 대상의 모든 객체가 모두 다 신호상태가 될때까지 대기하려면 TRUE를 하나라도 신호상태가 될때까지만 대기하려면 FALSE를 지정합니다.
- dwMilliseconds : 최대 대기 시간을 밀리초로 지정합니다.
 
WaitForMultipleObjects()함수의 리턴값은 다음과 같습니다.
fWaitAll이 TRUE일때와 FALSE일때 리턴되는 값이 다릅니다.
우선 TRUE일때는
- WAIT_OBJECT_0 : 감시대상의 모든 커널객체가 신호상태가 되었을 때 리턴함
FALSE일 때는
- WAIT_OBJECT_0 + 신호상태가된객체의인덱스번호 : 감시대상 커널객체 중 하나라도 신호상태가 되면 리턴합니다. 이 리턴값을 이용해서 lpHandles[리턴값-WAIT_OBJECT_0]으로 실제 신호상태가 된 객체를 알 수 있습니다.
- WAIT_ABANDONED_0 + 신호상태가된객체의인덱스번호 : 감시대상 커널객체 중 하나라도 신호상태가 되면 리턴합니다. 그런데 이 신호가 정상적으로 신호상태가 된게 아니라 다른 쓰레드로 부터 해당 객체가 포기되었다는 것을 나타냅니다. 이 리턴값을 이용해서 lpHandles[리턴값-WAIT_OBJECT_0]으로 실제 신호상태가 된 객체를 알 수 있습니다.
- WAIT_TIMEOUT : 지정한 타임 아웃 시간이 모두 경과하여 리턴함
- WAIT_FAILED : 대기에 실패했을 경우 리턴함
 
타임아웃값에 INFINITE라는 상수를 사용하게 되면 무한 대기 상태가 됩니다. 즉, 타임아웃을 지정하지 않는 것과 같습니다. 또한 해당 커널 객체를 포기하였다는 말은 정상적으로 객체의 소유권을 풀지 않고 쓰레드나 프로세스가 종료되었다는 의미입니다.
 
이 대기 함수들은 지금 이야기 하는 뮤텍스뿐만이 아니라 다음에 이야기 할 세마포어에서도 사용되어야 하므로 잘 기억해 두세요. 그리고 지금까지 윈도우즈 샘플 코드를 작성하면서 쓰레드의 종료를 대기하기 위해 메인 쓰레드에서 Sleep()을 사용했는데 대기함수를 공부했으므로 앞으로는 Sleep()함수 대신 대기함수를 사용하도록 하겠습니다.
 
그럼 이제 본격적으로 뮤텍스로 돌아와 보겠습니다.
윈도우즈의 뮤텍스와 관련된 함수는 다음과 것들이 있습니다.
 
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
BOOL ReleaseMutex(HANDLE hMutex);
 
CreateMutex()함수는 뮤텍스를 생성합니다.
이미 생성된 이름을 갖는 뮤텍스의 핸들을 얻기 위해서는 OpenMutex()를 사용합니다.
뮤텍스의 사용이 끝나서 해당 뮤텍스를 놓아 줄때는 ReleaseMutex()함수를 사용합니다.
위에 원형은 밝히지 않았지만 생성한 뮤텍스를 파괴시킬때는 모든 커널객체가 그렇듯 CloseHandle()함수를 사용합니다.
참고로 신호상태인 뮤택스를 얻기 위해서는 위에서 설명한 대기함수를 사용해야 한다는 것은 짐작하셨겠죠?
 
이때 CreateMutex()는 이미 다른 프로세스에서 사용하고자 하는 뮤텍스를 생성하였다면 CreateMutex()함수를 호출하지 않고 OpenMutex()를 사용해도 되지만 CreateMutex()함수를 다시 한번 더 호출해도 상관이 없습니다.
 
CreateMutex()함수의 아큐먼트는 다음과 같습니다.
- lpMutexAttributes : 뮤텍스의 보안 속성을 지정하는 것으로서 주로 상속관계를 지정하기 위해 사용됩니다. 일반적으로는 NULL을 입력합니다.
- bInitialOwner : 뮤텍스를 생성하면서 사용권한을 갖을 것인지를 결정합니다. 즉, TRUE를 입력하면 생성하자 마자 뮤텍스의 사용권한(비신호상태의 뮤텍스 생성)을 갖습니다. FALSE를 입력할 경우 뮤텍스만 생성하기만(신호상태의 뮤텍스 생성) 하고 실제 사용권한을 얻을때는 대기함수를 사용해야 합니다.
- lpName : 뮤텍스에 이름을 문자열로 지어 줍니다. 이름이 지정된 뮤텍스를 만듦으로서 이 이름을 아는 다른 프로세스와의 동기화를 할 수 있습니다. 하나의 프로세스에서만 동기화 방식으로 뮤텍스를 사용한다면 NULL을 입력하여 이름을 지어 주지 않을 수도 있습니다. 참고 이름을 지어준 뮤텍스를 명명된 뮤텍스(named mutex)라고 합니다.
 
CreateMutex()함수는 생성한 뮤텍스의 핸들을 리턴합니다. 만약 생성하려는 뮤텍스의 이름으로 이미 뮤텍스가 생성되어 있는 경우는 해당 뮤텍스 핸들을 리턴하고 GetLastError()로는 ERROR_ALREADY_EXISTS값을 얻을 수 있습니다. 만약 생성되어 있는 뮤텍스에 접근할 수 있는 권한이 없는 경우(ERROR_ACCESS_DENIED) NULL을 리턴합니다. 구체적인 원인을 알기 위해서는 GetLastError()함수를 호출하여 알 수 있습니다. 만약 EROR_ACCESS_DENIED일 경우는 OpenMutex()함수를 사용하여야 합니다.
 
OpenMutex()함수의 아큐먼트는 다음과 같습니다.
- dwDesiredAccess : 접근속성
- bInheritHandle : 상속옵션
- lpName : 지정된 뮤텍스의 이름
 
OpenMutex()함수는 이름이 지정된 뮤텍스의 핸들의 핸들을 리턴합니다. 만약 지정된 이름의 뮤텍스가 없는 경우는 NULL을 리턴하고 GetLastEror()함수는 ERROR_FILE_NOT_FOUND값을 리턴합니다.
 
일반적으로 OpenMutex()함수는 잘 사용하지 않습니다. 왜냐하면 CreateMutex()함수로 새로운 뮤텍스를 생성할 수도 이미 생성된 뮤텍스의 핸들을 얻을 수도 있기 때문이죠.
위에서 이야기 한대로 뮤텍스에 보안 속성을 설정한 경우 OpenMutex()를 사용하기도 합니다.
 
ReleaseMutex()함수는 잡았던 뮤텍스(비신호상태의 뮤텍스)를 다시 놓아주는(신호상태로 만들어 주는) 함수입니다.
아큐먼트는 hMutex에 뮤텍스 핸들을 넣어 줍니다. 놓아주기에 성공하면 TRUE를 실패하면 FALSE를 리턴합니다.
 
휴, 드디어 모든 뮤텍스 함수에 대해서 설명을 마쳤네요. ㅋㅋㅋ
이제 예제만 보여드리면 되겠네요.
 
#include <stdio.h> #include <process.h> #include <windows.h> int cnt=0; HANDLE hMutex; DWORD WINAPI Thread1(void *arg) { int i, tmp; for(i=0; i<1000; i++){ WaitForSingleObject(hMutex, INFINITE); tmp=cnt; Sleep(1); cnt=tmp+1; ReleaseMutex(hMutex); } printf("Thread1 End\n"); return 0; } DWORD WINAPI Thread2(void *arg) { int i, tmp; for(i=0; i<1000; i++){ WaitForSingleObject(hMutex, INFINITE); tmp=cnt; Sleep(1); cnt=tmp+1; ReleaseMutex(hMutex); } printf("Thread2 End\n"); return 0; } int main(int argc, char *argv[]) { HANDLE hThread[2]; hMutex=CreateMutex(NULL, FALSE, NULL); hThread[0]=CreateThread(NULL, 0, Thread1, NULL, 0, NULL); hThread[1]=CreateThread(NULL, 0, Thread2, NULL, 0, NULL); WaitForMultipleObjects(2, hThread, TRUE, INFINITE); printf("%d\n", cnt); CloseHandle(hThread[0]); CloseHandle(hThread[1]); CloseHandle(hMutex); return 0; }
 
앞의 예제에서 사용했던 소스를 뮤텍스를 사용해서 작성하였으므로 굳이 설명드리지 않아도 되겠죠? thread함수도 _beginethread()에서 CreateThread()함수로 바뀌었네요. 사실 _beginethread()함수가 리턴함 값을 HANDLE로 casting해서 hThread배열에 담아도 결과는 같습니다.
 
뮤텍스는 여기까지 입니다. 별로 어렵지 않죠?
 
Creative Commons License
Creative Commons License이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-동일조건변경허락 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
Copyright 조희창(Nicholas Jo). Some rights reserved. http://bbs.nicklib.com