동기화 기법의 분류와 CRITICAL_SECTION 동기화

유저모드와 커널모드

  • 유저모드: 응용 프로그램이 실행되는 기본 모드로, 물리적인 영역으로의 접근이 허용되지 않으며, 접근할 수 있는 메모리의 영역에도 제한이 따른다.
  • 커널모드: 운영체제가 실행될 때의 모드로, 메모리 뿐만 아니라, 하드웨어의 접근에도 제한이 따르지 않는다.

유저모드 동기화

운영체제의 도움 없이 응용 프로그램 상에서 진행되는 동기화가 바로 유저모드 동기화이다. 유저모드 동기화의 가장 큰 장점은 다음과 같다.

“속도가 빠르다.”

CRITICAL_SECTION 기반의 동기화 또한 유저모드 동기화의 일종이다.

커널모드 동기화

  • 유저모드 동기화에 비해 제공되는 기능이 더 많다.
  • Dead-Lock에 걸리지 않도록 타임아웃의 지정이 가능하다.

Mutex, Semaphore, Event 기반의 동기화가 커널모드 동기화의 일종이다.

CRITICAL_SECTION 기반의 동기화

CRITICAL_SECTION 기반의 동기화에서는 ‘CRITICAL_SECTION 오브젝트’라는 것을 생성해서 이를 동기화에 활용한다. 참고로 이는 커널 오브젝트가 아니며, 대부분의 다른 동기화 오브젝트와 마찬가지로 이는 임계영역의 진입에 필요한 일종의 ‘Key(열쇠)‘로 이해할 수 있다. 때문에 임계영역의 진입을 위해서는 CRITICAL_SECTION 오브젝트라는 열쇠를 얻어야 하고, 반대로 임계영역을 빠져나갈 때에는 얻었던 CRITICAL_SECTION 오브젝트(이하 CS 오브젝트)를 반납해야 한다.

이어서 CS 오브젝트의 초기화 및 소멸과 관련된 함수를 소개하겠다.

#include <windows.h>

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSecion);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
  • lpCriticalSection: Init… 함수에서는 초기화 할 CRITICAL_SECTION 오브젝트의 주소값 전달, 반면 Del… 함수에서는 해제할 CRITICAL_SECTION 오브젝트의 주소 값 전달.

참고로 위 함수의 매개변수형인 LPCRITICAL_SECTION은 CRITICAL_SECTION의 포인터 형이다. 그리고 DeleteCriticalSection 함수는 CRITICAL_SECTION 오브젝트를 소멸하는 함수가 아니다. 이 함수는 CRITICAL_SECTION 오브젝트가 사용하던(CRITICAL_SECTION와 연관되어 있는) 리소스를 소멸시키는 함수이다. 그럼 이어서 CS 오브젝트의 획득(소유) 및 반납에 관련된 함수를 소개하겠다. 단순하게는 열쇠의 획득 및 반납에 대한 함수로 이해해도 좋다.

#include <windows.h>

void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
  • lpCriticalSection: 획득(소유) 및 반납할 CRITICAL_SECTION 오브젝트의 주소 값 전달.

앞서 리눅스에서 소개한 뮤텍스와 상당히 유사하다.

커널 모드 동기화 기법들(Mutex, Semaphore, Event)

Mutex(Mutual Exclusion) 오브젝트 기반 동기화

Mutex 오브젝트 기반의 동기화도 CS 오브젝트 기반의 동기화와 유사하다. 따라서 Mutex 오브젝트 역시 열쇠에 비유해서 이해할 수 있다. 그럼 먼저 Mutex 오브젝트의 생성에 관련된 함수를 소개하겠다.

#include <windows.h>

HANDLE CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes, 
    BOOL bInitialOwner, 
    LPCTSTR lpName
);
// 성공 시 생성된 Mutex 오브젝트의 핸들, 실패 시 NULL 반환
  • lpMutexAttributes: 보안관련 특성 정보의 전달, 디폴트 보안설정을 위해서 NULL 전달
  • bInitialOwner: TRUE 전달 시, 생성되는 Mutex 오브젝트는 이 함수를 호출한 쓰레드의 소유가 되면서 non-signaled 상태가 된다. 반면 FALSE 전달 시, 생성되는 Mutex 오브젝트는 소유자가 존재하지 않으며, signaled 상태로 생성된다.
  • lpName: Mutex 오브젝트에 이름을 부여할 때 사용된다. NULL을 전달하면 이름없는 Mutex 오브젝트가 생성된다.

위의 매개변수 설명에서 언급했듯이, Mutex 오브젝트는 소유자가 없는 경우에 signaled 상태가 된다. 이러한 특성을 이용해서 동기화를 진행한다.

Mutex는 커널 오브젝트이기 때문에 다음 함수의 호출을 통해서 소멸이 이루어진다.

#include <windows.h>

BOOL CloseHandle(HANDLE hObject);
// 성공 시 TRUE, 실패 시 FALSE 반환
  • hObject: 소멸하고자 하는 커널 오브젝트의 핸들 전달.

위 함수는 커널 오브젝트를 소멸하는데 공통적으로 사용되는 함수이기 때문에, 이어서 소개하는 Semaphore와 Event의 소멸에도 사용된다. Mutex의 획득은 WaitForSingleObject의 함수호출을 통해서 이뤄지며, 반납은 다음 함수로 이루어진다.

#include <windows.h>

BOOL ReleaseMutex(HANDLE hMutex);
// 성공 시 TRUE, 실패 시 FALSE 반환
  • hMutex: 반납할, 다시 말해서 소유를 해제할 Mutex 오브젝트의 핸들 전달.

Mutex는 소유되었을 때 non-signaled 상태가 되고, 반납되었을 떄(소유되지 않았을 때) signaled 상태가 되는 커널 오브젝트이다. 따라서 Mutex의 소유여부를 확인할 때에는 WaitForSingleObject 함수를 이용할 수 있다.

  • 호출 후 블로킹 상태 : Mutex 오브젝트가 다른 쓰레드에게 소유되어서 현재 non-signaled상태에 놓여있는 상황.
  • 호출 후 반환된 상태 : Mutex 오브젝트의 소유가 해제되었거나 소유되지 않아서 signaled 상태에 놓여있는 상황

Mutex는 WaitForSingleObject 함수가 반환될 때, 자동으로 non-signaled 상태가 되는 ‘auto-reset 모드’ 커널 오브젝트이다. 따라서 WaitForSingleObject함수가 결과적으로 Mutex를 소유할 때 호출하는 함수가 된다. 그러므로 Mutex 기반의 임계영역 보호를 위한 코드는 다음과 같이 구성된다.

WaitForSingleObject(hMutex, INFINITE);
// 임계영역의 시작
// . . . . .
// 임계영역의 끝
ReleaseMutex(hMutex);

WaitForSingleObject함수는 반환하며 Mutex 커널 오브젝트를 non-signaled 상태로 만들어 다른 쓰레드의 임계영역으로의 접근을 차단한다.

ReleaseMutex함수는 Mutex 커널 오브젝트를 signaled 상태로 만들어 다른 쓰레드가 임계영역에 접근 가능하도록 해준다.

Semaphore 오브젝트 기반 동기화

윈도우의 ‘Semaphore 오브젝트 기반 동기화’ 역시, 리눅스의 세마포어와 유사하다. 둘 다 ‘세마포어 값(Semaphore Value)‘이라 불리는 정수를 기반으로 동기화가 이뤄지고, 이 값이 0보다 작아질 수 없다는 특징도 모두 동일하다. 세마포어 값은 커널 오브젝트에 등록된다.

#include <windows.h>

HANDLE CreateSemaphore(
    LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
    LONG lInitialCount,
    LONG lMaximumCount,
    LPCTSTR lpName
);
// 성공 시 생성된 Semaphore 오브젝트의 핸들, 실패 시 NULL 반환
  • lpSemaphoreAttributes: 보안관련 정보의 전달, 디폴트 보안설정을 위해서 NULL 전달
  • lInitialCount: 세마포어의 초기 값 지정, 매개변수 iMaximumCount에 전달된 값보다 크면 안되고, 0 이상이어야 한다.
  • lMaximumCount: 최대 세마포어 값을 지정한다. 1을 전달하면 세마포어 값이 0, 또는 1이되는 바이너리 세마포어가 구성된다.
  • lpName: Semaphore 오브젝트에 이름을 부여할 때 사용한다. NULL을 전달하면 이름없는 Semaphore 오브젝트가 생성된다.

세마포어 값이 0인 경우 non-signaled상태가 되고, 0보다 큰 경우에 signaled 상태가 되는 특성을 이용해서 동기화가 진행된다. 그리고 매개변수 lInitialCount에 0이 전달되면, non-signaled 상태의 Semaphore 오브젝트가 생성된다. 또한 매개변수 lMaximumCount에 3을 전달하면 세마포어의 최대 값은 3이기 때문에, 세 개의 쓰레드가 동시에 임계영역에 진입하는 유형의 동기화도 가능하다. 그럼 이어서 Semaphore 오브젝트의 반납에 사용되는 함수를 소개하겠다.

#include <windows.h>

BOOL ReleaseSemaphore(
    HANDLE hSemaphore,
    LONG lReleaseCount,
    LPLONG lpPreviousCount
);
// 성공 시 TRUE, 실패 시 FALSE 반환
  • hSemaphore: 반납할 Semaphore 오브젝트의 핸들 전달.
  • lReleaseCount: 반납은 세마포어 값의 증가를 의미하는데, 이 매개변수를 통해서 증가되는 값의 크기를 지정할 수 있다. 그리고 이로 인해서 세마포어의 최대 값을 넘어서게 되면, 값은 증가하지 않고 FALSE가 반환된다.
  • lpPreviousCount: 변경 이전의 세마포어 값 저장을 위한 변수의 주소 값 전달, 불필요하다면 NULL 전달

Semaphore 오브젝트는 세마포어 값이 0보다 큰 경우에 signaled상태가 되고, 0인 경우에 non-signaled 상태가 되게 한다. 따라서 다음의 형태로 임계영역의 보호가 가능하다.

WaitForSingleObject(hSemaphore, INFINITE);
// 임계영역의 시작
// . . . .
// 임계영역의 끝
ReleaseSemaphore(hSemaphore, 1, NULL);

Event 오브젝트 기반 동기화

Event 동기화 오브젝트는 ‘auto-reset’ 모드와 ‘manual-reset’ 모드를 선택할 수 있다는 특징이 있다.

#include <windows.h>

HANDLE CreateEvent(
    LPSECURITY_ATTRIBUTES lpEventAttributes,
    BOOL bManualReset,
    BOOL bInitialState,
    LPCTSTR lpName
);
// 성공 시 생성된 Event 오브젝트의 핸들, 실패 시 NULL 반환
  • lpEventAttributes: 보안관련 정보의 전달, 디폴트 보안설정을 위해서 NULL 전달.
  • bManualReset: TRUE 전달 시 manual-reset 모드 Event, FALSE 전달 시 auto-reset 모드 Event 오브젝트 생성.
  • bInitialState: TRUE 전달 시 signaled 상태의 Event 오브젝트 생성, FALSE 전달 시 non-signaled 상태의 Evnet 오브젝트 생성
  • lpName: Event 오브젝트에 이름을 부여할 때 사용된다. NULL을 전달하면 이름없는 Event 오브젝트가 생성된다.
#include <windows.h>

BOOL ResetEvent(HANDLE hEvent); // to the non-signaled
BOOL setEvent(HANDLE hEvent);   // to the signaled
// 성공 시 TRUE, 실패 시 FALSE 반환

Event 오브젝트의 핸들을 인자로 전달하면서 위 함수를 호출하면 된다.