▣ 동기화
⊙ 멀티 스레드의 문제점
동시에 복수 개의 코드가 같은 주소 영역에서 실행됨으로써 서로 간섭하고 영향을 주는 경우가 빈번한다.
운영체제는 이런 문제를 해결할 수 있는 방법을 제공해 주기는 하지만 아주 사소한 부분에서도 민감하게
문제가 발생할수 있으며 이런 문제는 디버깅하기도 아주 어렵다.
가장 큰 문제점은 공유 자원을 보호하기가 어렵다는 점이다. 여기서 공유자원이란 직렬 포트, 사운드 카드
등의 하드웨어가 될 수도 있지만 주로 메노리 영역의 전역변수가 해당된다. 동일한 프로세스에 속한 스레드는
같은 주소 공간에서 실행되며 전역변수를 공유하게 되므로 문제가 발생할 소지가 많다. 두 스레드가 같은
전역변수에 값을 대입할 경우 먼저 앞쪽 스레드가 대입한 값은 뒤쪽 스레드가 대입한 값에 의해 지워지게 된다.
이런 식으로 스레드가 공유 자원을 서로 사용하려는 상태를 경쟁상태(race condition)라고 한다.
또한 스레드간의 실행 순서를 제어하는 것도 쉽지 않은 문제이다. 최악의 경우 스레드끼리 서로를 기다리는
교착상태(deadlock)가 발생하기도 한다.
이런 여러기지 문제를 해결하기 위하여 스레드간의 실행 순서를 제어할수 있는 여러가지 방법들을 동기화
(Synchronization)라고 한다.
⊙ 크리티컬 섹션
운영 체제가 지원하는 동기화 방법은 스레드가 실행될 수 있는 상황인가를 판단해서 조건이 맞을 때까지
대기하도록 해준다.
동기화 방법에는 여러가지가 있는데 그중에서 크리티컬 섹션이 가장 이해하기 쉽고 속도도 빠르다. 다만
동일한 프로세스 내에서만 사용해야 하는 제약이 있다.
크리티컬 섹션은 다음 두 함수로 초기화 및 파괴를 한다.
▶VOID InitialiCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
▶VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
둘다 CRITICAL_SECTION형의 포인터를 인수로 요구하므로 CRITICAL_SECTION형의 변수를 선언하여 넘겨주면 된다.
단 이 변수는 복수 개의 스레드가 참조해야 하므로 반드시 전역변수로 선언해야 한다.
전역변수로 선언한 후 InitializeCriticalSection 함수로 초기화하면 이후부터 크리티컬 섹션을 사용 할수 있다.
프로세스 종료시에 DeleteCriticalSection으로 파괴해 주어야 한다.
크리티컬 섹션을 구성하는 함수
▶VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
▶VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
둘다 CRITICAL_SECTION형의 포인터를 인수로 취한다. 이 두 함수 사이의 코드가 바로 크리티컬 섹션이 된다.
EnterCriticalSection 함수는 이 코드가 크리티컬 섹션을 소유하도록 해 주며 이후부터 다른 스레드는 같은
크리티컬 섹션에 들어올 수 없게 된다. 만약 EnterCriticalSection 이 호출될 때 이미 다른 스레드가 크리티컬
섹션에 들어와 있으면 이 함수는 크리티컬 섹션이 해제(LeaveCriticalSection)될 때까지 안전하게 대기하도록 한다.
⊙ 교착 상태
교착 상태(deadlock)란 대기 상태가 종료되지 않아 무한정 대기만 하는 비정상적인 상태이다.Enter 함수는 다른
스레드가 크리티컬 섹션을 소유하고 있으면 대기 상태로 들어간다. 이 대기 상태는 스레드 외부에서 풀어주어야
하는데 만약 놀리적인 오류로 대기 상태를 풀어주지 못하면 교착 상태가 된다.
⊙ 백그라운드 작업의 동기화
스레드에게 복잡한 과정을 통해 값을 계산해 내는 일을 맏겼을때 주 스레드에서 이 함수를 호출하여 계산을 시켰
다면 이 함수는 계산이 완료되기 전에는 리턴하지 못하므로 그동안 주 스레드는 무작정 기다려야만 한다. 이럴땐
계산 함수를 별도의 스레드로 분리한다.
만약 주 스레드가 계산 결과를 화면에 출력해야 한다면 화면에 출력될 분량 만큼만 계산이 완료 되면 주스레드는
즉시 작업이 가능하다.
▣ 뮤텍스
동기화 객체(Synchronization Object)란 말 그대로 동기화에 사용되는 객체이다. 프로세스, 스레드처럼 커널 객체이며
프로세스 한정적인 핸들을 가진다. 동기화 객체는 크리티컬 섹션보다 느리기는 하지만 훨씬 더 복잡한 동기화에 사용될 수
있다.
■신호상태(Signaled) : 스레드의 실행을 허가하는 상태이다. 신호상태의 동기화 객체를 가진 스레드는 계속 실행할 수 있다.
■비신호상태(Nonsignaled) : 스레드의 실행을 허가하지 않은 상태이며 신호상태가 될 때까지 스레드는 블록된다.
동기화 객체는 대기 함수와 함께 사용되는데 대기 함수(Wait Function)는 일정한 조건에 따라 스레드의 실행을 블록하거나
실행을 허가하는 함수이다. 여기서 일정한 조건이란 주로 동기화 객체의 신호 여부가 된다.
▶ DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
이 함수는 hHandle이 지정하는 하나의 동기화 객체가 신호상태가 되기를 기다린다.
리턴값을 검사해 보면 어떤 이유로 대기상태를 종료했는지 알수 있다.
WAIT_OBJECT_0 hHandle 객체가 신호상태가 되었다.
WAIT_TIMEOUT 타임 아웃 시간이 경과하였다.
WAITABANDONED 포기된 뮤텍스
뮤텍스는 크리티컬 섹션과 여러가지 면에서 비슷하므로 크리티컬 섹션이 쓰이는 곳에 대신 사용될 수 있다. 그러나 크리티컬
센션 보다는 속도가 느리다. 뮤텍스는 오직 한 스레드에 위해서만 소유될 수 있으며 일단 어떤 스레드에게 소유되면 비신호
상태가 된다. 반대로 어떤 스레드에도 소유되어 있지 않은 상태라면 신호상태가 된다.
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);
보안 속성을 지정하는 첫번째 인수는 대개의 경우 NULL로 준다. bInitialOwner는 뮤텍스를 생성함과 동시에 소유할 것인지를
지정하는데 이 값이 TRUE 이면 이 스레드가 뮤텍스를 소유하며 뮤텍스가 비신호상태로 생성됨으로써 다른 스레드는 이 뮤텍스를
소유할수 없게 된다. 마지막 인수는 뮤텍스의 이름을 지정하는 문자열이다. 뮤텍스는 프로세서끼리의 동기화에도 사용되므로
이름을 가지는데 이 이름은 프로세스간에 뮤텍스를 공유할 때 사용된다. 뮤텍스에 이름이 있을 경우 다른 프로세스가 일단
뮤텍스의 이름만 알면 다음 함수로 뮤텍스의 핸들을 얻을 수 있다.
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
이 때 OpenMutex 함수가 리턴하는 뮤텍스 핸들은 원래의 뮤텍스를 가리키지만 핸들값은 원래의 핸들값과 다른 프로세스 한정적
핸들이다. CreateMutex 나 OpenMutex 함수가 지정하는 뮤텍스의 이름은 시스템 전역적으로 유일하게 하나의 뮤텍스를 가르킨다.
마치 프로세스의 ID가 시스템 전역적으로 유일한 것과 마찬가지다. 동일 프로세스 내에서만 뮤텍스를 사용한다면 뮤텍스의 이름
을 줄 필요는 없으면 CreateMutex 함수의 세번째 인수에는 NULL을 넘겨주면 된다.
생성한 뮤텍스를 파괴할 때는 모든 커널 객체와 마찬가지로 CloseHandle 함수를 사용하면 된다. CloseHandle은 뮤텍스 핸들을
닫으며 만약 이 핸들이 대상 뮤텍스를 가르키는 마지막 핸들이라면 뮤텍스 객체도 파괴한다.
BOOL ReleaseMutex(HANDLE hMutex); // 비신호상태의 뮤텍스를 다시 신호상태로 만든다.
▣ 대기 함수
1). 대기함수는 스레드의 실행을 블록시키는 역활을 한다. 여기서 블록(Block)시킨다는 말은 조건을 만족할 때까지 실행하지
모하도록 한다는 뜻이다.
2). 조건이 만족할 대까지 대기한다. 여기서 말하는 조건이란 대기함수에 따라 다르다.
3). 대기 중에서 CPU 시간을 거의 소비하지 않음으로써 효율적으로 대기 하도록 한다.
DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE *lpHandles, BOOL fWaitAll, DWORD dwMiliseconds);
WaitForSingleObject가 하나의 동기화 객체를 기다리는 대기 함수인 데 비해 이 함수는 복수 개의 동기화 객체를 대기할 수 있다.
대기하는 동기화 객체의 핸들 배열을 작성한 후 lpHandles 인수로 배열의 포인터를 전달해 주며 nCount로 배열의 크기, 즉 동기화
객체의 개수를 넘겨준다. fWaitAll이 TRUE이면 모든 동기화 객체가 신호상태가 될 때까지 대기하며 FALSE이면 그중 하나라도 신호
상태가 되면 대기 상태를 종료한다.두 개 이상의 스레드가 작업을 완료할 때까지 대기하거나 또는 둘 중 하나라도 먼저 작업을
마칠 때까지 대기하고자 할때 이 함수를 사용한다. 마지막 인수는 WaitForSingleObject와 마찬가지로 타임 아웃값이다.
리턴값의 의미는 WAIT_TIMEOUT(지정한 시간이 경과), bWaitAll이 TRUE인 경우 WAIT_OBJECT_0이 리턴 되면 모든 동기화 객체가
신호상태가 되었다는 뜻이며 bWaitAll이 FALSE인 경우 lpHandles 배열에서 신호상태가 된 동기화 객체의 인덱스를 리턴해준다.
이 경우 lpHandles[리턴값 - WAIT_OBJECT_0] 식으로 신호상태가 된 동기화 객체의 샌들을 구할 수 있다.
⊙ 프로세스간의 동기화
뮤텍스는 이름이 있으므로 프로세스간의 동기화에도 사용할 수 있다. 뮤텍스는 이벤트, 세마포어, 파일 맵핑 객체들과
같은 네임 스페이스를 공유하므로 커널 객체와도 이름이 중복되서는 안된다. 만약 이름이 중복된다면 CreateMutex는 뮤텍스
를 생성하지 못하고 에러를 내고만다.
⊙ 포기된 뮤텍스
한 스레드가 뮤텍스를 소유하면 이 뮤텍스를 기다리는 스레드들은 모두 블록 상태가 된다. 그런데 만약 뮤텍스를 소유한
스레드가 어떠한 이유로 뮤텍스의 소유를 풀지 못하게 되면 어떻게 될까? 구조적 예외가 발생한 경우나 아니면 ExitThread
로 스레드를 종료했거나 외부에서 TerminateThread로 스레드를 강제로 죽였을 경우 이런 현상이 발생할 수 있다. 크리티컬
섹션의 경우라면 블록된 스레드를 깨울수 있는 방법이 없지만 뮤텍스의 경우는 한가지 안전 장치가 있다.
뮤텍스는 자신을 소유한 스레드가 누구인지를 기억하고 있는데 시스템은 뮤텍스의 소유 스레드가 뮤텍스를 풀지않고
종료되엇을 경우 강제로 뮤텍스를 신호상태로 만들어 준다. 이때의 뮤텍스를 포기된 뮤텍스(Abandoned Mutex)라고 한다.
뮤텍스가 포기되면 대기중인 스레드중 하나가 뮤텍스를 가지게 될것이다. 이때 이 스레드는 WaitForSingleObject함수의
리턴값으로 WIAT_ABANDONED 값을 전달받음으로써 이 뮤텍스가 정상적인 방법으로 신호상태가 된 것이 아니라 포기 된
것임을 알수 있다.
⊙ 중복 소유
한 스레드가 뮤텍스를 소유하고 있는 상황에서 다른 스레드는 이 뮤텍스를 소유하지 못한다. 그런데 만약 같은 스레드가
뮤텍스를 두번 소유하고자 한다면 어떻게 될까? 이경우 첫번째 디기 함수에서 hMutex를 소유하고 이 뮤텍스를 비신호로
만들 것이다. 그럼 두 번째 대기함수를 호출할때 뮤텍스가 비신호 상태이므로 무한정 대기해야 하겠지만 만약 그렇게 된다면
이 상태를 교착상태와 같아진다. 그러나 뮤텍스는 자신의 소유주를 기억함은 물론이고 소유된 횟수도 기억하고 있다.
그래서 이미 자신을 소유한 스레드가 다시 한번 자신을 소유하고자 할 경우는 해당 스레드를 블록시키지 않고 소유 횟수만
증가시킨다. 즉 스레드는 같은 뮤텍스를 여러번 소유할 수 있다. 다시 신호 상태로 만들기 위해서는 ReleaseMutex를 소유한
횟수만큼 호출해 주어야 한다.
⊙ 신호상태 조사
WaitForSingleObject 함수는 뮤텍스가 비신호상태이면 스레드를 블록 시킨다. 이 함수의 타임아웃 인수를 0으로 넘겨주면
곧바로 신호상태를 조사해서 리턴해준다.
▣ 세마포어
⊙ 제한된 자원
세마포어는 뮤텍스와 유사한 동기화 객체이다. 물론 차이점이 있다. 뮤텍스는 하나의 공유 자원을 보호하기 위해 사용하지만
세마포어는 그렇지 않다. 세마포어는 제한된 일정 개수를 가지는 자원을 보호하고 관리한다. 여기서 자원이라 함은 상당히
추상적인 개념인데 어떠한 것이라도 가능하다. 세마포어는 사용가능한 자원의 개수를 카운트하는 동기화 객체이다. 유효 자원이
0 이면 즉 하나도 사용할 수 없으면 세마포어는 비신호 상태가 되면 1이상이면 신호상태가 된다.
▶HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName);
▶HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
CreateSemaphore 함수로 세마포어를 생성하되 lMaximumCount에 최대 사용 개수를 지정하고 lInitialCount에 초기값을 지정하면
된다. 아주 특별한경우를 제외 하고 보통 이 두 값은 동일 하다. 세마포어는 뮤텍스와 마찬가지로 이름을 가질 수 있고 이름을 알고
있는 프로세스는 언제든지 OpenSemaphore 함수로 세마포어의 핸들을 구할수 있다. 세마포어는 커널 객책이므로 파괴할 때는
CloseHandle을 호출해 주면 된다.
▶BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount); //세마포어의 카운트를 증가시키는 함수
lReleaseCount로 자신이 사용한 자원의 개수를 알려주는데 하나만 사용했으면 이값은 1이고 만약 여러개의 자원을 사용했다면
사용한 만큼 자원을 풀어주면 된다.
⊙ SemaphoreThree
세마포어를 사용하면 일정 개수만큼만 실행되는 프로그램도 만들수 있다.
WinMain의 선두에서 최대 카운트 3의 세마포어를 만들되 다른 프로세스끼리 공유해야 하므로 반드시 이름을 주어야 한다. 같은 이름의
세마포어가 이미 만들어져 있다면 그 세마포어를 가르키는 핸들을 리턴해 줄것이다. 이 핸들로 WaitForSingleObject함수를 호출하되
타임 아웃을 0으로 주어 핸들이 신호상태인가만 조사 한다. 만약 비신호 상태라면 이미 세개의 세마포어가 모두 사용중이라는 뜻이며
세개의 인스턴스가 이미 실행 됬다는 ㄸㅅ이므로 조용히 프로그램을 종료하면 된다. 단 이때 CloseHandle함수로 세마포어의 핸들을
닫아주는 것을 잊으면 안된다.
▣ 이벤트
- 어떤 사건이 일어 났음을 알려주는 동기화 객체이다. 이벤트를 기다리는 스레드는 이벤트가 신호 상태가 될 때까지 대기하여 신호상태가 되면
대기를 풀고 작업을 시작한다. 이벤트는 리셋되는 방식에 따라 두 가지 종류가 있다.
■ 자동 리셋 이벤트 : 대기 상태가 종료되면 자동으로 비신호 상태가 된다.
■ 수동 리셋 이벤트 : 스레드가 비신호상태로 만들어줄 때까지 신호상태를 유지한다.
▶HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName);
▶HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
bManualReset은 이 이벤트가 수동 리셋 이벤트인지 자동 리셋 이벤트인지를 지정하는데 TRUE 이면 수동 리셋 이벤트가 된다.
bInitialState가 TRUE이면 이벤트를 생성함과 동시에 신호상태로 만들어 이벤트를 기다리는 스레드가 곧바로 실행을 하도록 해준다.
▶BOOL SetEvent(HANDLE hEvent); // 이벤트를 신호 상태로 만든다.
▶BOOL ResetEvent(HANDLE hEvent); // 이벤트를 비신호 상태로 만든다.
⊙ BackEvent
이벤트를 대기하던 스레드가 이벤트의 신호를 받으면 다음 신호를 또 기다리기 위해 이벤트를 다시 비 신호 상태로 만들어 놓아야
한다. 이 작업이 대기 함수에 의해 자동으로 이루어지므로 자동 리셋 이벤트라고 한다.
⊙ 수동 리셋 이벤트
수동 리셋 이벤트는 대기함수가 리턴될 때 신호상태를 그대로 유지하며 ResetEvent 함수로 일부러 비신호상태로 만들어 줄 때만
상태가 변경된다. 그래서 여러 개의 스레드가 하나의 이벤트를 기다리고 있더라도 한 번의 신호로 대기하던 모든 스레드가 일제히
작업을 시작할 수 있다. 수동 레셋 이벤트를 다시 비신호 상태로 만들어 중어야 하는 시점은 이 이벤트를 기다리는 모든 스레드가
대기 상태에서 풀려났을 때이다.
▶BOOL PulseEvent(HANDLE hEvent);
이 함수는 SetEvent를 호출하여 이벤트를 신호상태를 만든다.그리고 대기하던 스레드가 대기 상태를 벗어나면 다시 이벤트를
비신호 상태로만든다.
⊙ DownEvent
다운로드 작업은 완벽한 백그라운드 작업이며 스레드를 쓰기에 가장 적합하다. 수동 리셋이벤트로 생성한다.
수동 리셋 이벤트는 대기 함수에 의해 상태가 강제로 변환되지 않으므로 다운로드 작업에 유용하다.
▣ 그 외의 동기화 객체
⊙ WaitChild
차일드 프로세스가 실행되면 완전히 독립적인 프로세스가 된다. 프로세스를 생성한 후 그 핸들이 신호상태가 될 때까지 무한
대기하는 것이다. 프로세스 객체도 동기화 목적에 사용할수 있으므로 WaitForSingleObject 대기 함수의 인수로 사용 할 수
있으며 이때 대기 함수는 프로세스 객체가 신호상태가 될 때까지, 즉 프로세스가 종료될 때까지 효울적으로 대기해 준다.
⊙ 모탈 프로세스
만약 윈도우를 만드는 차일드라면 페어런트 프로세스가 차일드 종료시까지 대기하다 보니 실행이 완전히 블록 상태가 되어
어떠한 메시지도 응답을 못하게 된다. 이 문제를 해결하려면 프로세스 객체가 신호상태가 될 때까지 무한으로 대기하지 말고
비신호 상태인 동안에도 메시지를 처리하도록 해주면 된다.