임계 영역
: 둘 이상의 쓰레드가 동시에 메모리에 접근하도록 만드는 코드 블럭.
유저 모드 동기화커널 모드 동기화 동기화 기법은 단순히 제공되는 것. 내부 동작 이해를 위해서는 하드웨어에 대한 이해가 필요하기 때문에 사용법 정도만 알아두기.
CRITICAL_SECTION gCriticalSection; // critical section object. -> 열쇠 생성.
InitializeCriticalSection(gCriticalSection); // 크리티컬 섹션 기반 동기화를 내부적으로 처리 하기 위한 초기화
// 최소한의 기본작업을 요청하는 함수. 내부적으로 무슨일 하는지는 중요하지 않음.
// ... 중략 ...
// 임계 영역 진입을 위한 크리티컬 섹션 오브젝트 획득. 즉 열쇠 획득.
// 여러 쓰레드 중, 빠른 한 명만 열쇠를 얻고 나머지는 Blocked 상태로 빠지게됨.
EnterCriticalSection(&gCriticalSection);
// ... 임계 영역 ...
// 크리티컬 섹션 오브젝트 반환. 즉 열쇠 반환.
LeaveCriticalSection(&gCriticalSection);
// ... 중략 ...
// 열쇠 소멸.
DeleteCriticalSection(&gCriticalSection);
임계 영역에 대한 접근은 이처럼 열쇠의 획득과 반환의 반복.
임계 영역은 넓게 잡을 수록 안정적. 그러나 임계 영역이 넓을 수록 둘 이상의 쓰레드가 접근할 수 있는 범위가 제한되기 때문에 성능 저하가 일어날 수 있다.
따라서 임계 영역을 구성할 때는 필요한 부분과 필요하지 않은 부분을 철저하게 구분해 최소한의 영역만 잡아야 한다.
/*
CriticalSectionSync.cpp
프로그램 설명: 생성 가능한 쓰레드의 개수 측정.
*/
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>
#define NUM_OF_GATE 6
LONG gTotalCount = 0;
CRITICAL_SECTION hCriticalSection;
void IncreaseCount()
{
EnterCriticalSection (&hCriticalSection);
gTotalCount++; // 대부분 임계 영역이 이런식으로 한두줄임. 그래서 좀 불합리해 보일 수 있음.
// why? 크리티컬 섹션 개체의 선언->초기화->획득->반환->소멸의 과정이 너무 길어서.
LeaveCriticalSection (&hCriticalSection);
}
unsigned int WINAPI ThreadProc( LPVOID lpParam )
{
for(DWORD i=0; i<1000; i++)
{
IncreaseCount();
}
return 0;
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadId[NUM_OF_GATE];
HANDLE hThread[NUM_OF_GATE];
InitializeCriticalSection(&hCriticalSection);
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
hThread[i] = (HANDLE)
_beginthreadex (
NULL,
0,
ThreadProc,
NULL,
CREATE_SUSPENDED,
(unsigned *)&dwThreadId[i]
);
if(hThread[i] == NULL)
{
_tprintf(_T("Thread creation fault! \n"));
return -1;
}
}
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
ResumeThread(hThread[i]);
}
WaitForMultipleObjects(NUM_OF_GATE, hThread, TRUE, INFINITE);
_tprintf(_T("total count: %d \n"), gTotalCount);
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
CloseHandle(hThread[i]);
}
DeleteCriticalSection(&hCriticalSection);
return 0;
}
원자적 접근을 보장. 즉 한 순간에 하나의 쓰레드에 의한 호출만 허용.
void IncreaseCount(void)
{
InterlockedIncrement(&gTotalCount); // ~= ++gTotalCount. 원자적 접근(Atomic Access)을 보장.
}
하드웨어는 어떤 현상이 일어났음을 Interupt를 통해 OS에 알림. ex) 클럭 발생
OS는 Timer Interupt를 통해 시간의 흐름을 체크.
Interupt를 Disable 시키는 것은 Interupt 신호를 OS가 인지하는 것을 막는 것. 따라서 OS는 시간이 흘렀는지 알 수 없기 때문에 스케줄러가 동작하지 않아 컨텍스트 스위칭이 일어나지 않음.
이런 방식으로 A 쓰레드가 임계 영역에 접근할 때 Timer Interupt를 Disable시켜두면 컨텍스트 스위칭을 막을 수 있고, A 쓰레드가 완전히 지나갈 때 까지 다른 쓰레드의 실행을 막을 수 있다.
OS에서 말하는 동기화 기법 중 세마 포어가 있고, 그 중 바이너리 세마 포어를 뮤텍스라고 함.
세마 포어와 뮤텍스의 가장 큰 차이점은 임계 영역에 진입하기 위한 Key의 개수.
임계 영역에 진입하기 위해서는 Key가 필요한데 세마 포어의 Key는 여러 개이고, 뮤텍스의 Key는 한 개.
즉 세마 포어는 임계 영역에 접근하는 쓰레드의 개수를 Key의 개수로 제한할 수 있다. 뮤텍스의 경우 임계 영역에 동시 접근할 수 있는 쓰레드가 한 개.
뮤텍스라는 열쇠를 생성하는 함수.
// If the below function fails, the return value is NULL.
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // 보안 관리자.
// 커널 모드의 동기화 기법이기 때문에, 커널 오브젝트의 생성을 동반하기 때문에 보안관리자 필요.
// 핸들 테이블의 핸들이 상속 여부를 결정할 때 사용했었음.
BOOL bInitialOwner, // 소유자 지정
// 열쇠를 만든사람이 초기에 소유하게끔 할거냐 아니면 누구나 소유하게 할거냐 여부.
LPCTSTR lpName // 뮤텍스 이름 지정
// 이름 관련 개념은 이후에 나옴.
);
뮤텍스를 커널 오브젝트라고 생각.
WaitForSingleObject(): 뮤텍스가 Signaled 상태가 되기를 기다리는 함수.
ReleaseObject(): 뮤텍스를 Non-Signaled 상태로 만드는 함수.
1) Key를 가지고 WaitForSingleObject() 함수를 호출. 현재 뮤텍스가 Signaled 상태라면 함수를 빠져나오면서 쓰레드가 임계 영역으로 진입. 이후 뮤텍스를 Non-Signaled 상태로 전환.
2) 쓰레드가 임계 영역을 빠져나오면서 ReleaseObject() 함수를 호출. 뮤텍스를 Signaled 상태로 전환.
3) WaitForSingleObject() 함수를 호출하며 대기 중에 있던 다음 쓰레드는 함수를 반환하며 임계 영역에 진입.

/*
CriticalSectionSyncMutex.cpp
프로그램 설명: 크리티컬 섹션과 뮤텍스 비교. 얼마나 유사한지를 보자.
*/
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>
#define NUM_OF_GATE 6
LONG gTotalCount = 0;
// CRITICAL_SECTION gCriticalSection;
HANDLE hMutex;
void IncreaseCount()
{
// EnterCriticalSection (&gCriticalSection);
WaitForSingleObject(hMutex, INFINITE);
gTotalCount++;
// LeaveCriticalSection (&gCriticalSection);
ReleaseMutex(hMutex);
}
unsigned int WINAPI ThreadProc( LPVOID lpParam )
{
for(DWORD i=0; i<1000; i++)
{
IncreaseCount();
}
return 0;
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadIDs[NUM_OF_GATE];
HANDLE hThreads[NUM_OF_GATE];
// InitializeCriticalSection(&gCriticalSection);
hMutex = CreateMutex (
NULL, // 디폴트 보안관리자.
FALSE, // 누구나 소유 할 수 있는 상태로 생성.
NULL // numaned mutex
);
if (hMutex == NULL)
{
_tprintf(_T("CreateMutex error: %d\n"), GetLastError());
}
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
hThreads[i] = (HANDLE)
_beginthreadex (
NULL,
0,
ThreadProc,
NULL,
CREATE_SUSPENDED,
(unsigned *)&dwThreadIDs[i]
);
if(hThreads[i] == NULL)
{
_tprintf(_T("Thread creation fault! \n"));
return -1;
}
}
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
ResumeThread(hThreads[i]);
}
WaitForMultipleObjects(NUM_OF_GATE, hThreads, TRUE, INFINITE);
_tprintf(_T("total count: %d \n"), gTotalCount);
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
CloseHandle(hThreads[i]);
}
// DeleteCriticalSection(&gCriticalSection);
CloseHandle(hMutex);
return 0;
}
// If the below function fails, the return value is NULL.
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttribures,
LONG lInitialCount, // 열쇠의 개수. 보통은 세마포어의 카운트라고 부름.
// "세마포어의 값이 3이다" == "열쇠 꾸러미에 열쇠가 3개다."
// 즉, 임계영역에 총 3개의 쓰레드가 동시접근 가능하게 하겠단 뜻.
LONG lMaximumCount, // 세마포어가 가질 수 있는 카운트의 최댓값 지정.
// 즉, lInitialCount보다 작은건 말이 안됨. 항상 lInitialCount <= lMaximumCount.
LPCTSTR lpName // 잠시 후에 나오는 개념.
);
ReleaseSemaphore(): 세마포어 값이 1 증가
WaitForSingleObject(): 세마포어 값을 1 감소
세마포어 카운트는 값이 0이 되기 전까지는 계속 Signaled 상태. 따라서 WaitForSingleObject() 함수가 호출되어도 바로 Blocked 상태가 되지 않는다.
즉 세마포어 카운트만큼의 쓰레드가 임계 영역에 동시 접근이 가능하다.

/*
MyongDongKyoJa.cpp
프로그램 설명: 카운트 세마포어에 대한 이해
시뮬레이션 제한 요소:
1. 테이블이 총 10개이고, 동시에 총 10분의 손님만 받을 수 있다고 가정한다.
2. 오늘 점심시간에 식사하러 오실 예상되는 손님의 수는 총 50분이다.
3. 각 손님들께서 식사 하시는 시간은 대략 10분에서 30분 사이이다.
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>
#define NUM_OF_CUSTOMER 50
#define RANGE_MIN 10
#define RANGE_MAX (30 - RANGE_MIN)
#define TABLE_CNT 10
HANDLE hSemaphore;
DWORD randTimeArr[50];
void TakeMeal(DWORD time)
{
WaitForSingleObject(hSemaphore, INFINITE);
_tprintf( _T("Enter Customer %d~ \n"), GetCurrentThreadId());
_tprintf(_T("Customer %d having launch~ \n"), GetCurrentThreadId());
Sleep(1000 * time); // 식사중인 상태를 시뮬레이션 하는 함수.
ReleaseSemaphore(hSemaphore, 1, NULL);
_tprintf( _T("Out Customer %d~ \n\n"), GetCurrentThreadId());
}
unsigned int WINAPI ThreadProc( LPVOID lpParam )
{
TakeMeal((DWORD)lpParam);
return 0;
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadIDs[NUM_OF_CUSTOMER];
HANDLE hThreads[NUM_OF_CUSTOMER];
srand( (unsigned)time( NULL ) ); // random function seed 설정
// 쓰레드에게 전달할 random 값 총 50개 생성.
for(int i=0; i<NUM_OF_CUSTOMER ;i++)
{
randTimeArr[i] = (DWORD) (
((double)rand() / (double)RAND_MAX) * RANGE_MAX + RANGE_MIN
);
}
// 세마포어 생성.
hSemaphore = CreateSemaphore (
NULL, // 디폴트 보안관리자.
TABLE_CNT, // 세마포어 초기 값.
TABLE_CNT, // 세마포어 최대 값.
NULL // unnamed 세마포어 구성.
);
if (hSemaphore == NULL)
{
_tprintf(_T("CreateSemaphore error: %d\n"), GetLastError());
}
// Customer를 의미하는 쓰레드 생성.
for(int i=0; i<NUM_OF_CUSTOMER; i++)
{
hThreads[i] = (HANDLE)
_beginthreadex (
NULL,
0,
ThreadProc,
(void*)randTimeArr[i],
CREATE_SUSPENDED,
(unsigned *)&dwThreadIDs[i]
);
if(hThreads[i] == NULL)
{
_tprintf(_T("Thread creation fault! \n"));
return -1;
}
}
for(int i=0; i<NUM_OF_CUSTOMER; i++)
{
ResumeThread(hThreads[i]);
}
WaitForMultipleObjects(NUM_OF_CUSTOMER, hThreads, TRUE, INFINITE);
_tprintf(_T("----END-----------\n"));
for(int i=0; i<NUM_OF_CUSTOMER; i++)
{
CloseHandle(hThreads[i]);
}
CloseHandle(hSemaphore);
return 0;
}
서로 다른 프로세스 영역에 존재하는 쓰레드 간의 동기화도 가능하다.
1) 프로세스 A가 열쇠 역할을 할 뮤텍스를 생성 -> 뮤텍스를 두 쓰레드가 공유하기 때문에 둘 중 한 프로세스만 만들면 됨.

2) 뮤텍스에 접근하기 위해서는 핸들 정보가 필요한데, 프로세스 B는 프로세스 A가 만든 뮤텍스의 정보를 모름. 현재 프로세스 A 내에서 생성된 뮤텍스는 해당 프로세스에 있는 쓰레드만 접근 가능한 상태. 핸들 정보는 프로세스 A의 핸들 테이블에 등록되어 있기 때문에 프로세스 A 안에 있는 쓰레드에게만 유효한 값. 다른 프로세스에게는 의미가 없다.
3) 프로세스 A의 핸들 테이블에 등록된 뮤텍스의 핸들 정보를 프로세스 B의 핸들 테이블에도 등록해야 뮤텍스에 접근 가능할 것. 같은 프로세스 내 쓰레드 간의 동기화의 경우, 핸들을 이용했다. 이 경우에도 핸들을 이용한다는 건 같지만, 다른 프로세스에 있기 때문에 핸들을 직접 얻어올 수 없는 상황. 따라서 프로세스 B는 이름을 통해 뮤텍스를 찾아와 등록한다.
4) CreateMutex() 함수의 경우 뮤텍스의 이름을 지정할 수 있음. 이러한 뮤텍스를 이름 있는 뮤텍스 라고 한다.
5) OpenMutex() 함수를 통해 뮤텍스의 정보를 얻어온다. 즉, 핸들 테이블에 뮤텍스를 등록.

/*
NamedMutex.cpp
프로그램 설명: named mutex의 역할 이해.
*/
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>
HANDLE hMutex;
DWORD dwWaitResult;
void ProcessBaseCriticalSection()
{
dwWaitResult = WaitForSingleObject(hMutex, INFINITE);
switch (dwWaitResult)
{
// 쓰레드가 뮤텍스를 소유하였다.
case WAIT_OBJECT_0:
_tprintf(_T ("thread got mutex ! \n") );
break;
// time-out 발생하였다.
case WAIT_TIMEOUT:
_tprintf(_T ("timer expired ! \n") );
return;
// 뮤텍스 반환이 적절이 이뤄지지 않았다.
case WAIT_ABANDONED:
return;
}
for(DWORD i=0; i<5; i++)
{
_tprintf( _T("Thread Running ! \n") );
Sleep(10000);
}
ReleaseMutex(hMutex);
}
int _tmain(int argc, TCHAR* argv[])
{
#if 1 // 1이면 CreateMutex() 호출되고, 0이면 OpenMutex() 호출됨.
hMutex = CreateMutex(
NULL,
FALSE,
_T("NamedMutex")
);
#else
hMutex = OpenMutex(
MUTEX_ALL_ACCESS,
FALSE,
_T("NamedMutex")
);
#endif
if (hMutex == NULL)
{
_tprintf(_T("CreateMutex error: %d\n"), GetLastError());
return -1;
}
ProcessBaseCriticalSection();
CloseHandle(hMutex);
return 0;
}
뮤텍스의 경우 세마 포어와 달리 소유의 개념이 존재.
1) 쓰레드 A가 열쇠를 얻은 상태에서 임계 영역에 진입.
2) 쓰레드 B는 WaitForSingleObject() 함수 내에서 대기 중.
3) 이 때 쓰레드 A는 임계 영역을 빠져나온 뒤 열쇠를 반환해야 하는데,
그냥 종료되버림. 열쇠를 소유한 쓰레드가 반환해줘야 하는데, 반환하지 않고 종료되버렸으니 쓰레드 B는 막연해진 상황.
4) 이 때 쓰레드 B는 WaitForSingleObject() 함수를 빠져나오면서 WAIT_ABANDONED라는 값을 반환.
해당 값은 정상적이지 않은 종료에 의해 뮤텍스 반환이 이루어지지 않았으니 윈도우즈라는 OS가 열쇠를 다시 주겠다는 뜻.
쓰레드 A가 종료되었을 경우 OS는 문제를 인지하고 이를 추적함. 그리고 쓰레드 B에게 열쇠를 전달.
세마포어는 카운트 값을 기준으로 뮤텍스를 공유하기 때문에, 값을 -한 대상과 +한 대상이 일치하지 않아도 문제 없음. 그러나 두 대상이 일치하지 않는 구조는 좋은 구조라 할 수 없음.
이러한 소유의 개념은 모든 OS에 공통되는 개념이 아니라 윈도우즈의 특성.
/*
MUTEX_WAIT_ABANDONED.cpp
프로그램 설명: 뮤텍스와 관련된 반환값 WAIT_ABANDONED에 대한 설명.
*/
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>
LONG gTotalCount = 0;
HANDLE hMutex;
unsigned int WINAPI IncreaseCountOne(LPVOID lpParam)
{
WaitForSingleObject(hMutex, INFINITE);
gTotalCount++;
return 0;
}
unsigned int WINAPI IncreaseCountTwo(LPVOID lpParam)
{
DWORD dwWaitResult = 0;
dwWaitResult = WaitForSingleObject(hMutex, INFINITE);
switch (dwWaitResult)
{
case WAIT_OBJECT_0:
ReleaseMutex(hMutex);
break;
case WAIT_ABANDONED:
_tprintf( _T("WAIT_ABANDONED \n") );
break;
}
gTotalCount++;
ReleaseMutex(hMutex);
return 0;
}
int _tmain(int argc, TCHAR** argv)
{
DWORD dwThreadIDOne;
DWORD dwThreadIDTwo;
HANDLE hThreadOne;
HANDLE hThreadTwo;
hMutex = CreateMutex (
NULL, // 디폴트 보안관리자.
FALSE, // 누구나 소유 할 수 있는 상태로 생성.
NULL // numaned mutex
);
if (hMutex == NULL)
{
_tprintf(_T("CreateMutex error: %d\n"), GetLastError());
}
// 무례한 쓰레드.
hThreadOne = (HANDLE)_beginthreadex (
NULL, 0, IncreaseCountOne, NULL, 0, (unsigned *)&dwThreadIDOne
);
hThreadTwo = (HANDLE)_beginthreadex (
NULL, 0, IncreaseCountTwo, NULL, CREATE_SUSPENDED, (unsigned *)&dwThreadIDTwo
);
Sleep(1000);
ResumeThread(hThreadTwo);
WaitForSingleObject(hThreadTwo, INFINITE);
_tprintf(_T("total count: %d \n"), gTotalCount);
CloseHandle(hThreadOne);
CloseHandle(hThreadTwo);
CloseHandle(hMutex);
return 0;
}