※ 아래는 윤성우 뇌를 자극하는 윈도우즈 프로그래밍 한빛미디어(주) 2022년
Chapter13(p.411 ~ 449)를 읽고 정리한 내용입니다.
동기화란 보통 동일한 상태(일치함)를 의미하지만
쓰레드 관점에서 동기화는 질서가 잘 지켜지고 있음을 의미한다.
실행순서의 동기화 : 쓰레드의 실행순서를 정의하고 이 순서에 반드시 따르도록 하는 것
메모리 접근에 대한 동기화 : 메모리 접근에 있어서 동시접근을 막는 것
(1) 유저 모드 동기화
동기화가 진행되는 과정에서 커널의 힘을 빌리지 않는 동기화 기법
-> 커널 모드로의 전환이 불필요하므로 성능상에 이점이 있음
-> 그만큼 기능상의 제한도 있음
(2) 커널 모드 동기화
커널에서 제공하는 동기화 기능을 활용하는 기법
-> 동기화에 관련된 함수가 호출될 때 마다 커널 모드로 변경이 필요하므로 성능이 저하됨
-> 유저 모드 동기화에서 제공하지 못하는 기능을 제공받을 수 있음
배타적 접근(한 순간에 하나의 쓰레드만 접근)이 요구되는 공유 리소스(전역변수와 같은)에
접근하는 코드 블록
-> 둘 이상의 쓰레드가 동시에 실행될 경우 문제가 발생되는 코드 블록 (메모리가 아니다)
(참고) 운영체제 교과서에서는 임계 영역 접근 동기화를 가리켜 "상호 배제 동기화"라 함
(1) 유저 모드 동기화 : 크리티컬 섹션 기반의 동기화, 인터락 기반의 동기화
(2) 커널 모드 동기화 : 뮤텍스 기반의 동기화, 세마포어 기반의 동기화,
이름있는 뮤텍스 기반의 프로세스 동기화, 이벤트 기반의 동기화
(참고) 이벤트 동기화는 실행순서 동기화에서 사용하는 기법임
화장실 앞에 걸어 놓은 화장실 열쇠에 비유하기
열쇠를 얻은 자만이 화장실에 들어 갈 수 있고, 다 쓴 후에는 열쇠를 다시 걸어놔야 한다.
(예제)
(1) 크리티컬 섹션 오브젝트를 만들고 초기화한다.
CRITICAL_SECTION gCriticalSection; // Critical Section Object
void IniticalizeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
int _tmain(int argc, TCHAR* argv[])
{
InitializeCriticalSection(&gCriticalSection); // Initialize Object
}
(2) 화장실 열쇠 얻기
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
// lpCriticalSection 임계 영역(화장실)에 진입하기 위해 필요한
// 크리티컬 섹션 오브젝트의 주소값을 인자로 전달한다.
// 이미 이 함수가 다른 쓰레드에 의해 호출된 상태라면 호출된 함수는 블로킹된다.
// 그리고 열쇠가 반환되면 블로킹 상태에 있던 함수는 빠져 나온다.
(3) 화장실 열쇠 반납하기
void LeaveCriticalSection(&CriticalSection);
(4) 초기화 함수가 호출되는 과정에서 할당된 리소스 반환하기
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
LONG InterlockedIncrement(LONG volatile* Addend);
// Addend : 값을 하나 증가시킬 32비트 변수의 주소값을 전달한다.
// 둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을 이 함수를 통해서 증가시킬 경우,
// 동기화된 상태에서 접근하는 것과 동일한 안정성을 보장받을 수 있다.
LONG InterlockedDecrement(LONG volatile* Addend);
// Addend : 값을 하나 감소시킬 32비트 변수의 주소값을 인자로 전달한다.
// 둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을 이 함수를 통해서 감소시킬 경우,
// 동기화된 상태에서 접근하는 것과 동일한 안정성을 보장받을 수 있다.
Volatile 키워드는 다음 두 가지 의미를 지닌다.
(1) 뮤텍스(뮤텍스 오브젝트)를 만든다. (초기화 함수 호출 불필요, 생성과 동시에 초기화됨)
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, // 보안 속성 지정
BOOL bInitialOwner, // false는 먼저 차지하면 기회를
// true는 뮤텍스를 생성하는 쓰레드가 기회를 얻음
LPCRSTR lpName); // 뮤텍스에 이름을 붙이기 위한 용도
// 해당 함수의 반환 타입인 HANDLE은 뮤텍스가 커널 오브젝트임을 의미함
// 뮤텍스는 누군가에 의해 획득이 가능할 때 Signaled 상태에 놓임
// 따라서 WaitForSingleObject 함수를 임계 영역 진입을 위한 뮤텍스 획득의 용도로 사용 가능
// WaitForSingleObject 함수는 인자로 전달된 핸들의 커널 오브젝트가
// Signaled 상태가 되어서 반환되는 경우,
// 해당 커널 오브젝트를 Non-Signaled 상태로 변경해 버린다.
// -> 다른 쓰레드들은 임계 영역으로 진입이 불가 해 진다.
(2) 뮤텍스를 반환한다.
BOOL ReleaseMutex(HANDLE hMutex); // 반환할 뮤텍스의 핸들을 인자로 전달함
// Non-signaled 상태에 있는 뮤텍스는 Signaled 상태가 됨
(3) 뮤텍스는 커널 오브젝트이므로 핸들을 반환한다. (CloseHandle 함수 호출)
뮤텍스는 세마포어의 일종이다.
세마포어는 뮤텍스와 달리 카운트(Count) 기능이 있다.
-> 즉, 세마포어는 임계 영역에 접근 가능한 쓰레드 개수를 조절할 수 있다.
(세마포어 카운트를 1로 두면 뮤텍스라 할 수 있다.
(1) 세마포어 생성
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 보안 속성 지정
LONG lInitialCount, // 임계 영역에 접근 가능한 쓰레드 개수
LONG lMaximumCount, // 세마포어가 지닐 수 있는 값의 최대 크기
// 기본적으로 lInitialCount로 전달되는 값보다 커야 함
LPCTSTR lpName); // 세마포어에 이름을 붙이기 위한 용도
(2) 세마포어 반환
BOOL ReleaseSemaphore(HANDLE hSemaphore, // 반환하고자 하는 세마포어의 핸들
LONG lReleaseCount, // 세마포어 카운트 증가 개수
// 2 전달 시 카운트는 2씩 증가
LPLONG lpPreviousCount // 변경되기 전 세마포어 카운트 값
뮤텍스는 획득한 쓰레드가 직접 반환해야하나 세마포어와 그 이외의 동기화 오브젝트는
마치 도서 대여점처럼 다른 쓰레드가 반환해도 문제 되지 않는다.

뮤텍스는 커널 오브젝트이므로 서로 다른 프로세스 영역에 존재하는 쓰레드가 뮤텍스를
이용해서 동기화 가능하다.
다만, 핸들 테이블은 독립적이므로 뮤텍스에 접근하기 위해서는 OpenMutex 함수를 이용해야 한다.
HANDLE OpenMutex(DWORD dwDesiredAccess, // 이름있는 뮤텍스로의 접근 권한(MUTEX_All_ACCESS)
BOOL bInheritHandle, // 핸들의 상속 유무 결정
LPCTSTR lpName); // 얻고자 하는 핸들 정보의 커널 오브젝트 이름 전달
// 해당 이름과 일치하는 이름을 가진 뮤텍스가 존재한다면,
// 이 뮤텍스의 핸들이 반환된다.
// 핸들 테이블에 이에 대한 정보도 추가된다.