티스토리 뷰

General/Parerell

멀티스레드 동기화

엘키 2014. 10. 1. 12:38

1.Critical Section

 - 유저 레벨의 동기화 방법 중, 유일하게 커널 객체를 사용하지 않음.

 - 내부 구조가 단순하여 동기화 처리에 대한 속도가 빠르다.

 - 동일한 프로세스 내에서만 사용.

 - 커널 객체를 사용하지 않기 때문에 핸들을 사용하지 않고, CRITICAL_SECTION 이라는 타입을 정의하여 사용.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 크리티컬 섹션을 초기화한다.
// 파라메터는 여러 개의 쓰레드에 참조되어야 하므로 전역으로 선언하도록 한다.
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
 
// 생성된 크리티컬 섹션을 삭제한다.
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
 
// 동기화 방법
void workFunc()
{
    EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection); // 호출. lock.
 
    // 여기서 공유 자원을 안전하게 액세스한다.
 
    LeavCriticalSection(LPCRITICAL_SECTION lpCriticalSection); // 호출. unlock.
}

2. 동기화 대기 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMiliseconds);
// hHandle : 동기화 객체.  dwMiliseconds : 대기 시간
// 대기 시간을 INFINITE로 지정하면 무한대로 대기한다.
// 반환 값의 종류는 세가지다.
//  WAIT_OBJECT_0 : 성공
//  WAIT_TIMER : 동기화 객체가 시그널 상태
//  WAIT_ABANDONED : 대기시간 초과
 
DWORD WaitForMulipleObject(DWORD nCound, CONST HANDLE *lpHandles,
                            BOOL fWaitAll, DWORD dwMiliseconds);
// WaitforSingleObject 함수는 하나의 객체에 대한 동기화를 기다리는데 비해 이 함수는
// 복수 개의 동기화 객체를 대기할 수 있다. 동기화 객체의 핸들 배열을 만든 후 lpHandles 인수로
// 배열의 포인터를 전달해 주고 nCount로 배열의 개수를 넘겨준다.
// fWaitAll이 TRUE이면 모든 동기화 객체가 시그널 상태가 될 때까지 대기하며,
// FALSE이면 그 중 하나라도 시그널 상태가 되면 대기 상태를 종료한다.
// 리턴 값의 의미는 조금 다르다.
//  WAIT_TIMEOUT : 대기시간 초과
//  WAIT_OBJECT_0 : bWaitAll이 TRUE일 때 - 모든 동기화 객체가 시그널 상태.
//      FALSE일 때 - lpHandles 배열에서 시그널 상태가 된 동기화 객체의 인덱스를 반환.
//      이 경우 lpHandles[리턴값 - WAIT_OBJECT_0]의 방법으로 시그널 상태가 된
//      동기화 객체의 핸들을 얻을 수 있다.

 

3. Mutex (MUTual EXclusion)

 - 최초 Signaled 상태로 생성되며, WaitforSingleObject()와 같은 대기 함수를 호출함으로써 NonSignaled 상태가 된다.

 - 크리티컬 섹션에 비해서 느리다. 크리티컬 섹션의 경우 구조체의 값을 통해 잠그기를 허용하는데 비해 뮤텍스는 객체를 생성하기 때문이다.

 - 만약 A라는 쓰레드가 뮤텍스를 소유하고 있고, B라는 쓰레드가 뮤텍스를 사용하기 위하여 대기하고 있을 때, A 쓰레드가 잘못된 연산을 수행하거나 강제 종료되어서 소유하고 있던 뮤텍스를 반환하지 않았을 때에 B라는 쓰레드는 뮤텍스를 얻기 위해 무한정 기다려야 할까?  => 만약 크리티컬 섹션을 사용하였다면 B 쓰레드는 무한정 대기하기 된다.(데드락) 하지만, 뮤텍스의 경우는 자신이 소유한 쓰레드가 누군지 기억하고 있다. 그리고 Windows 운영체제에서 뮤텍스를 반환하지 않는 상태에서 쓰레드가 종료될 경우 그 뮤텍스는 강제적으로 Signaled 상태로 만든다.

 - 만약 같은 쓰레드가 중복으로 뮤텍스를 호출할 경우 데드락이 발생할까? => 발생하지 않는다. 왜냐면 같은 쓰레드가 중복으로 뮤텍스를 호출할 경우는 내부 Count만 증가시키고, 진입은 허용한다. 그리고 나중에 내부 Count가 '0'으로 될 때 Singaled 상태로 해준다. (이 부분은 크리티컬 섹션도 동일한 개념이다.)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);
// lpMutexAttributes는 보안 속성으로 보통 NULL로 지정한다.
// bInitialOwner는 뮤텍스 생성과 동시에 소유할 것인지 지정하는데 TRUE이면 이 쓰레드가 바로 뮤텍스를
// 소유하면서 다른 쓰레드는 소유할 수 없게 된다.
// lpName은 뮤텍스의 이름이다. NULL 설정 가능.
// 리턴 값은 뮤텍스의 핸들이다.
 
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
// 뮤텍스를 연다. 프로세스 ID와 마찬가지로 뮤텍스의 이름은 전역적으로 유일하다.
 
BOOL ReleaseMutex(HANDLE hMutex);
// 해당 쓰레드의 뮤텍스 소유를 해제하여 다른 쓰레드가 가질 수 있도록 한다.
 
HRESULT CloseHandle(HANDLE hHandle);
// 모든 커널 객체와 마찬가지로 생성된 뮤텍스를 파괴할 때 사용한다.
// 리턴 값 S_OK : 성공.
//           그외엔 에러.

* 포기된 뮤텍스

 만약 뮤텍스를 소유하고 있는 쓰레드가 ExitThread나 TerminateThread로 비정상적으로 종료시켰을 경우 강제로 뮤텍스를 시그널 상태로 만들어준다. 그러므로 대기 중인 다른 쓰레드에서 뮤텍스를 가지게 되는데, WaitForSingleobject() 의 리턴 값으로 WAIT_ABANDONED 값을 전달 받음으로써 이 뮤텍스가 정상적인 방법으로 신호상태가 된 것이 아니라 포기된 상태임을 알 수 있다. 중복 소유 뮤텍스를 여러 번 겹쳐서 사용했을 경우 데드락과 같은 상태에 빠질 수도 있을 것이다.

 

4. Semaphore

 - 뮤텍스와 유사한 동기화 객체이다. 뮤텍스는 하나의 공유 자원을 보호하는데 비해, 세마포어는 일정 개수를 가지는 자원을 보호할 수 있다. 여기서 자원이라는 것은 윈도우, 프로세서, 쓰레드와 같은 SW적이거나 어떤 권한과 같은 무형적인 것도 포함된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG iInitialCount,
                                      LONG iMaximumCount, LPCTSTR lpName);
// iMaximumCount는 최대 사용 개수.
// iInitialCount 에 초기값을 지정.
// 아주 특별한 경우외엔 이 두가지 값은 같다. 세마포어는 뮤텍스와 같이 이름을 가질 수 있고 이름을 알고 있는
// 프로세스는 언제든지 OpenSemaphore로 핸들을 구할 수 있다. 역시 파괴할 때는 CloseHandle 함수를 이용한다.
 
HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSRT lpName);
// 뮤텍스와 같다.
 
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG iReleaseCount, LPLONG lpPreviousCount);
// iReleaseCount로 사용한 자원의 개수를 알려준다. lpPreviousCount는 세마포어 이전 카운트를 리턴받기 위한
// 참조 인수이다. NULL 가능.

 

5. Event

 - 위의 동기화 객체들이 공유자원을 보호하기 위해 사용되는데 비해 이벤트는 쓰레드의 작업 순서나 시기를 조정하기 위해 사용된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset,
                             BOOL bInitialState, LPCTSTR lpName);
// bManualReset은 이벤트가 수동 리셋(쓰레드가 Non-Signaled로 만들어 줄 때 까지 신호 상태를 유지)인지
// 자동리셋(대기 상태가 종료되면 자동으로 Non-Signaled가 된다.) 인지를 결정한다. TRUE이면 수동이다.
// bInitialState가 TRUE이면 자동으로 Signaled 상태로 들어가 이벤트를 기다리는 쓰레드가 곧바로 실행할 수 있게 된다.
 
HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSRT lpName);
// 다른 부분과 같다.
 
BOOL SetEvent(HANDLE hEvent);
// 다른 동기화 객체와는 다르게 사용자 임의로 Signaled 상태와 Non-Signaled 상태를 설정할 수 있다.
// 위의 함수는 Signaled 상태로 만들어 준다.
 
BOOL ResetEvent(HANDLE hEvent);
// Non-Signaled 상태로 만든다. 일반적으로 자동 리셋을 사용하는데, 이벤트를 발생시켜 대기 상태를 풀 때
// 자동으로 Non-Signaled 상태로 만드는 것이다. 하지만 여러 개의 쓰레드를 위해서 이벤트를 사용한다면
// 문제가 될 수도 있다. 그러므로 수동 리셋으로 이벤트를 생성 후 ResetEvent 함수로 수동리셋을 한다.

출처:: http://blog.daum.net/thermidor/5173637

댓글