멀티쓰레드는 개요에서 말했듯이 한 프로세스를 여러 역할에 따라 여러 개의 쓰레드로 나뉘어 작업하는 방식이므로 각 쓰레드간의 동기화가 필요하다.
동시에 복수개의 코드가 같은 주소영역에서 실행됨으로써 서로 간섭하고 영향을 주는 경우가 빈번하기 때문이다.
멀티쓰레드의 가장 큰 문제점은 공유자원(주로 메모리의 전역변수)을 보호하기가 어렵다는 점이다.
그리고 쓰레드간의 실행순서를 제어하는 것도 쉽지 않은 문제이다.
이런 여러가지 문제점을 해결하기 위하여 쓰레드간의 실행 순서를 제어할 수 있는 여러가지 방법들을 동기화라고 한다.
동기화 방법에는, Interlocked, 임계영역, 뮤텍스, 세마포어, 이벤트등의 기법을 사용한다.
1) 임계영역 (Critical Section)
동기화문제를 해결하는 방법들 중 가장 쉬운반면 동일한 프로세스 내에서만 사용해야 하는 제약이 있다.
임계영역(Critical Section)이란 공유자원의 독점을 보장하는 코드의 영역을 가리킨다. 이는 아래 두 함수로 시작하고 끝낸다.
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
CRITICAL_SECTION형의 포인터형은 복수개의 쓰레드가 참조해야 하므로 반드시 전역변수로 선언해야한다. 사용법은 다음과 같다.
CRITICAL_SECTION crit1, crit2;
함수 {
…
EnterCriticalSection(&crit1);
//공유자원1을 액서스한다.
LeaveCriticalSection(&crit1);
EnterCriticalSection(&crit2);
//공유자원2을 액서스한다.
LeaveCriticalSection(&crit2);
…
}
주의할것은 가급적 임계영역 내부의 코드가 빨리 끝날 수 있도록 짧은 시간을 사용하도록 작성해야 한다.
만약 Leave를 호출하지않고 쓰레드를 빠져나와버리면 이후부터는 다른 쓰레드는 이 임계영역에 들어갈 수 없게된다.
만약 이부분에서 예외가 발생하여 Leave함수가 호출되지 못하게 될 수도 있다.
그래서 임계영역을 쓸 때는 반드시 구조적 예외 처리구문에 포함시켜주는 것이 좋다.
Try {
EnterCriticalSection(&crit);
…
}
finally {
LeaveCriticalSection(&crit);
}
이렇게하면 설사 예외가 발생하더라도 Leave함수는 반드시 호출되므로 훨씬 안전해진다.
다음은 MFC 에서의 사용 예이다.
CCriticalSection g_critical; // 전역 변수로 선언
function() { AfxBeginThread(ThreadFuncA, NULL);
AfxBeginThread(ThreadFuncB, this);
}
UINT ThreadFuncA(LPVOID pParam) { while(1) { g_critical.Lock(); // ThreadFuncA가 할 일....
g_critical.Unlock(); }
return 0; }
UINT ThreadFuncB(LPVOID pParam) { while(1) { g_critical.Lock(); // ThreadFuncB가 할 일....
g_critical.Unlock(); } return 0; }
2) 뮤텍스(Mutex)
계영역은 앞서 말했듯 동일한 프로세스 내에서만 사용할 수 있다.
그러나, 뮤텍스(Mutex; Mutual Exclusion;상호배제)는 임계영역이 사용된 곳에 대신 사용될 수 있으며, 프로세스 간에도 사용할 수 있다.
뮤텍스를 사용하려면 다음 함수로 생성해야 한다.
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL blInitialOwner, LPCTSTR lpName);
lpMutexAttributes : 보안속성. 대개 NULL
blInitialOwner : 뮤텍스 생성과 동시에 소유할 것인지 지정.
lpName: 뮤텍스의 이름을 지정하는 문자열.
뮤텍스는 프로세스간의 동기화에도 사용되므로 이름이 필요하고, 이 이름은 프로세스간 뮤텍스를 공유할 때 사용된다.
뮤텍스 소유를 해지하여 다른 쓰레드가 이것을 가질 수 있도록 하려면 임계영역의 LeaveCriticalSection 에 해당하는 다음 함수를 호출하면 된다.
BOOL ReleaseMutex(HANDLE hMutex);
만일 프로세스가 다른 프로세스의 쓰레드에 의해서 이미 생성된 뮤텍스의 핸들을 얻기를 원하거나,
뮤텍스가 존재하지 않는 경우에 뮤텍스를 생성하기 원한다면 다음 함수를 사용한다.
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
3) 세마포어 (Semaphore)
세마포어도 뮤텍스와 유사한 동기화 객체이나 다른점은, 뮤텍스는 하나의 공유자원을 보호하기 위해 사용하지만,
세마포어는 제한된 일정 개수를 가지는 자원(HW, 윈도우, 프로세스, 쓰레드, 권한, 상태 등 컴퓨터에서의 모든 자원)을 보호하고 관리한다.
세마포어는 사용 가능한 자원의 개수를 카운트하는 동기화 객체이다.
세마포어와 관련된 함수는 다음과 같다.
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG IlInitialCount,
LONG lMaximumCount, LPCTSTR lpName);
HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);
4) 이벤트 (Event)
임계영역, 뮤텍스, 세마포어는 주로 공유자원을 보호하기 위해 사용되는 데 비해
이벤트는 이보다는 스레드간의 작업순서나 시기를 조정하기 위해 사용한다.
특정한 조건이 만족될 때까지 대기해야 하는 쓰레드가 있을 경우 이 쓰레드의 실행을 이벤트로 제어할 수 있다.
이벤트는 자동리셋과 수동리셋이 있다.
+자동 리셋 이벤트 : 대기상태가 종료되면 자동으로 비신호상태가 된다.
+수동 리셋 이벤트 : 쓰레드가 비신호상태로 만들어줄 때까지 신호상태를 유지한다.
++신호상태 (Signaled): 쓰레드 실행가능상태. 신호상태의 동기화 객체를 가진 쓰레드는 계속 실행할 수 있다.
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset,
BOOL bInitialState, LPCTSTR lpName);
HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
bManualReset은 이벤트가 수동리셋 이벤트(manual)인지 자동리셋 이벤트(automatic)인지 지정하는데 TRUE이면 수동리셋 이벤트가 된다.
bInitialState가 TRUE이면 이벤트를 생성함과 동시에 신호상태로 만들어 이벤트를 기다리는 쓰레드가 곧바로 실행을 하도록 해준다.
이벤트도 이름(lpName)을 가지므로 프로세스간의 동기화에 사용될 수 있다.
또한 이벤트가 임계영역이나 뮤텍스와 다른점은
대기함수를 사용하지 않고도, 쓰레드에서 임의적으로 신호상태와 비신호상태를 설정할 수 있다는 점이다. 다음 함수를 사용한다.
BOOL SetEvent(HANDLE hEvent);
BOOL ResetEvent(HANDLE hEvent);
SetEvent는 신호상태로 만들고 ResetEvent는 비신호상태로 만든다.
다음은 MFC 에서의 사용 예이다.
CEvent g_event; // 전역변수로 선언 FunctionA() {
AfxBeginThread(ThreadFunc, this);
}
FunctionB() {
g_event.SetEvent(); // Lock() 함수에서 더 이상 진행하지 못하고 잠자고 있는 쓰레드를 깨워서 일을 시키려면
// SetEvent()를 호출.
} // ThreadFunc() 함수는 이벤트가 발생할 때마다 while문을 한번씩 실행.
UINT ThreadFunc(LPVOID pParam) {
while(1) {
g_event.Lock(); // SetEvent()가 호출되면, Lock()함수에서 실행이 중단된 쓰레드가 다음 코드를 실행.
// ThreadFunc가 할 일....
g_event.Unlock();
}
return 0;
}