뇌를 자극하는 윈도우즈 시스템 프로그래밍(저자, 윤성우)
쓰레드를 생성하는 것도 중요하지만, 더 중요한 것은 쓰레드 간에 발생할 문제의 소지를 미연에 차단하는 것도 중요하다. 동기화 이슈는 런타임에 발생하기에 사소한 것이라도 쉽게 눈에 띄지 않는다. 그렇기 때문에 미리 런타임에 발생 가능한 오류를 예측하고 이를 방지해야 한다.
01. 쓰레드의 동기화란 무엇인가?
무엇인가를 일치시키는 것을 동기화라 생각할 수 있지만, 쓰레드 동기화에서의 동기화는 순서에 있어서 질서가 지켜지고 있음을 의미한다.
01. A. 실행 순서의 동기화
쓰레드에서는 실행 순서가 중요한 경우가 있다. A 쓰레드가 계산한 결과를 B 쓰레드가 받아서 출력하는 경우, A 쓰레드가 끝나고 B 쓰레드가 실행되어야만 한다. 즉, 쓰레드의 실행 순서를 정의하고 해당 실행 순서를 따르도록 만드는 것이 쓰레드 동기화다.
01. B. 메모리 접근에 대한 동기화
한 순간에 하나의 쓰레드만 접근해야 하는 메모리 영역이 존재한다. 대표적으로 데이터와 힙인데, 데이터 영역에 할당된 변수를 둘 이상의 쓰레드가 동시에 접근할 경우 문제가 발생한다. 그러므로 메모리 접근에 동시 접근을 막는 것 또한 쓰레드의 동기화에 해당한다.
01. C. 쓰레드 동기화에 있어서의 두 가지 방법
Windows에서는 다양한 동기화 기법을 제공한다. 동기화 기법을 제공하는 주체에 따라 유저 모드 동기화User Mode Synchronize 기법이고, 커널 모드 동기화Kernel Mode Synchronize 기법이다.
- 유저 모드 동기화
동기화가 진행되는 과정에서 커널의 힘을 빌리지 않는 동기화 기법이다. 커널 모드로의 전환이 불필요하기 때문에 성능에 이점이 있으나, 기능 상에 제한도 존재한다.
- 커널 모드 동기화
커널에서 제공하는 동기화 기능을 활용한 방법이다. 동기화 관련 함수 호출 시, 커널 모드로의 변경이 필요하고 이는 성능 저하의 원인이다. 하지만, 유저 모드에서 제한되는 기능의 사용이 가능해진다.
02. 임계 영역Critical Section 접근 동기화
메모리 영역의 접근을 동기화한다는 것은 임계 영역의 접근을 동기화하겠다는 뜻으로 해석할 수 있다. 그렇다면 임계 영역이 무엇을 의미하는지 보자.
02. A. 임계 영역Critical Section에 대한 이해
전역변수가 있을 경우, 다양한 쓰레드에서 동시에 전역 변수에 접근하고자 할 때, 문제가 생길 수 있다. 이러한 문제를 일으키는 코드 블럭을 임계 영역이라고 한다. 그러니까, 문제의 원인이 될 수 있는 코드 블록을 가리켜서 임계 영역으로 칭하지, 전역변수에 할당된 메모리 공간을 임계 영역이라 부르는 것은 아니다.
정리하자면, 임계 영역은 배타적 접근이 요구되는 공유 리소스에 접근하는 코드 블록을 의미한다. 이렇게 임계 영역으로 생기는 문제를 예방하고자 임계 영역은 한순간에 하나의 쓰레드만 실행시킬 수 있도록 동기화 기법을 사용해 주면 되는데, Windows에서 제공해 주는 기법들은 다음과 같다.
제공 주체 | 명칭 | 설명 |
유저 모드 동기화 | 크리티컬 섹션Critical Section 기반 동기화 |
메모리 접근 동기화에 사용할 예정 |
유저 모드 동기화 | 인터락 함수Interlocked Family Of Function 기반 동기화 |
메모리 접근 동기화에 사용할 예정 |
커널 모드 동기화 | 뮤텍스Mutex 기반 동기화 |
메모리 접근 동기화에 사용할 예정 |
커널 모드 동기화 | 세마포어Semaphore 기반 동기화 |
메모리 접근 동기화에 사용할 예정 |
커널 모드 동기화 | 이름있는 뮤텍스Named Mutex 기반 프로세스 동기화 |
프로세스간 동기화에 사용할 예정 |
커널 모드 동기화 | 이벤트Event 기반 동기화 |
실행순서 동기화에 사용할 예정 |
위 6개의 동기화 기법들은 특별히 용도가 정해져 있는 것은 아니다. 그러나 목적에 적합한 동기화를 사용할 경우, 간결하고 정확한 코드가 나오기 때문에 각자 목적에 대해서 숙지할 필요가 있다.
03. 유저 모드 동기화Synchronization in User Mode
03.A. 크리티컬 섹션Critical Section 기반의 동기화
임계 영역을 방에 비유하면, 임계 영역에 들어가기 위해서 열쇠가 필요하다. 방의 문 앞에 열쇠가 걸려있으면, 이를 통해서 방에 들어갈 수 있고 볼 일이 끝나면, 원래 있었던 장소에 열쇠를 다시 둔다. 이러한 방식이 크리티컬 섹션 기반의 동기화 방식이다.
이 기법을 적용하는 데에 필요한 것은 크리티컬 섹션 오브젝트인데, 이는 만들고 초기화를 해야 한다. 크리티컬 섹션 기반의 동기화 기법을 위해 필요한 것들을 알아보자.
크리티컬 섹션 오브젝트는 자료형 CRITICAL_SECTION의 변수를 말한다. 다음과 같이 선언하여 사용할 수 있다.
CRITICAL_SECTION gCriticalSection;
이후에는 아래의 함수를 통해서 초기화 과정을 거쳐주어야 한다.
void InitializeCriticalSection(
// 초기화하고자 하는 크리티컬 섹션 오브젝트의 주소값을 인자로 전달한다.
LPCRITICAL_SECTION lpCriticalSection
);
또, InitializeCriticalSection 함수가 열쇠를 가져오는 동작을 하는 메서드라면, 아래의 함수들을 통해서 열쇠를 원래 있었던 장소에 둘 수 있고, 접근할 수 있다.
void EnterCriticalSection(
// 임계 영역으로의 접근을 위한 크리티컬 섹션 오브젝트의 주소값을 인자로 전달한다.
// 다른 쓰레드에 의해 함수가 호출된 상태에서 호출된다면,
// 호출된 함수는 블로킹 된다.
// 열쇠가 반환되면 블로킹 상태에 있던 함수는 빠져나와, 함수 호출에 성공한다.
// 임계 영역으로 접근했을 때, 호출한 쓰레드가 크리티컬 섹션 오브젝트를 흭득했다 표현한다.
LPCRITICAL_SECTION lpCriticalSection
);
void LeaveCriticalSection(
// 임계 영역을 빠져 나오고 호출하는 함수이다. 다시 열쇠를 걸어두는 역할을 한다.
// EnterCriticalSection 함수를 호출하고 블로킹 상태의 쓰레드가 있다면,
// 이 함수 호출 시에 블로킹 상태를 빠져나와 임계 영역으로 접근하게 된다.
// 블로킹 상태에서 빠져나온다면 호출한 쓰레드가 크리티컬 섹션 오브젝트를 반환했다고 표현한다.
LPCRITICAL_SECTION lpCriticalSection
);
마지막으로 초기화 함수가 호출되는 과정에서 할당된 리소스를 반환하는 함수이다.
void DeleateCriticalSection(
// 반환하고자 하는 크리티컬 섹션 오브젝트의 주소값을 인자로 전달한다.
LPCRITICAL_SECTION lpCriticalSection
);
03.B. 인터락 함수Interlocked Family Of Function 기반의 동기화
앞선 예제와 같이, 전역 변수에 접근하는 것을 동기화하는 것이 목적이라면 인터락 함수를 사용하는 것도 나쁘지 않다. 인터락 함수는 내부적으로 한 순간에 하나의 쓰레드만 실행되도록 동기화되어 있다.
대표적인 인터락 함수 두 가지는 다음과 같다.
LONG InterlockedIncrement(
// 값을 하나 증가시킬 32비트 변수의 주소값을 전달한다.
// 둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을 이 함수를 통해 증가시킬 경우,
// 동기화된 상태에서 접근하는 것과 동일한 안전성을 보장받는다.
LONG volatile* Addend
);
LONG InterlockedDecrement(
// 값을 하나 감소시킬 32비트 변수의 주소값을 전달한다.
// 둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을 이 함수를 통해 감소시킬 경우,
// 동기화된 상태에서 접근하는 것과 동일한 안전성을 보장받는다.
LONG volatile* Addend
);
InterlockedIncrement 함수와 InterlockedDecrement 함수는 원자적 접근Atomic Access, 즉 한 순간에 하나의 쓰레드만 접근하는 것을 보장해 주는 함수다. 앞서 소개한 크리티컬 섹션 동기화 기법도 내부적으로는 인터락 함수 기반으로 구현되어 있다. 또, 유저 모드를 기반으로 하여 속도가 상당히 빠르다.
- volatile 키워드
volatile 키워드는 두 가지 의미를 지닌다, 하나는 '최적화를 수행하지 마라'는 것인데, 컴파일러는 프로그래밍된 코드를 컴파일하는 과정에서 최적화를 수행한다.
// 최적화 전
int function(void)
{
int a = 10;
a = 20;
a = 30;
cout << a;
);
// 최적화 후
int function(void)
{
int a = 30;
cout << a;
};
최적화 전의 코드와 최적화 후의 코드는 동일하게 a 변수에 담긴 30을 출력하고 있다. 다만, 초기화 전의 과정에서 필요 없는 값이 담겨 있는 절차를 변경하여 최적화했을 뿐이다.
굉장히 편해 보이지만, 이러한 최적화가 문제가 되는 상황이 있다. 중간 값이 생략이 되는 것이므로, 해당 값이 필요한 경우에는 마지막 값만 도출되고 있기 때문에 문제가 생기는 경우다.
이러한 경우에 volatile 키워드를 사용하여 최적화를 하지 않는다.
다음 두 번째 의미는 '메모리에 직접 연산하라'는 것이다. 캐쉬 메모리라는 개념이 있는데, 우리가 일정 시간에 특별한 행동을 수행하고 싶을 때, 캐쉬 메모리에 저장이 된다면 언젠가 메모리에 저장이 되기 때문에 해당 행동이 수행은 되나, 일정 시간에 행동이 어려울 것이다.
이를 방지하고자 volatile 키워드를 통해서 실행할 수 있다.
앞서의 인터락 함수들의 선언을 보면 포인터로 선언된 전달인자가 volatile로 선언되어 있음을 볼 수 있다. 전달되는 포인터를 이용해서 함수 내부적으로 최적화하지 않으며, 포인터가 가리키는 메모리 영역을 캐쉬하지 않겠다는 것을 의미한다.
04. 커널 모드 동기화Synchronization in Kernel Mode
04. A. 뮤텍스Mutex 기반의 동기화
크리티컬 섹션 기법과 비슷한 개념으로 열쇠에 해당하는 것이 뮤텍스 오브젝트고, 이를 생성하는 것은 다음 함수를 통해서 만들어진다.
HANDLE CreateMutex(
// 프로세스 생성 시, 보안 속성을 지정해준다. 핸들 상속 여부를 결정지을 수 있다.
// 프로세스도 커널 오브젝트이며, 뮤텍스도 커널 오브젝트이기 때문이다.
LPSECURITY_ATTRIBUTES lpMutexAttributes,
// 뮤텍스 오브젝트를 생성하는 쓰레드에 우선 접근 권한을 부여한다.
// TRUE -> 우선 접근 권한 부여
// FALSE -> 크리티컬 섹션과 동일하다.
BOOL bInitialOwner,
// 뮤텍스의 이름을 붙일 때 사용된다. 이름은 NULL 문자로 끝나는 문자열로 지정하면 된다.
// 이름을 준 뮤텍스를 이름있는 뮤텍스Named Mutex로 표현한다.
LPCTSTR lpName
);
뮤텍스는 초기화 함수 호출이 필요 없으며, 위 함수를 호출하는 과정에서 모든 초기화가 이루어진다. 그리고 뮤텍스는 커널 오브젝트이기 때문에 상태가 존재하는데, Signaled 상태가 되기 위해서는 누군가에게 흭득이 가능할 때어야 한다.
따라서 WaitForSingleObject 함수를 임계 영역 진입을 위한 뮤텍스 흭득의 용도로 사용 가능하다. 반면, 뮤텍스를 반환 할 경우에는 다음 함수를 이용한다. 뮤텍스는 다시 Signaled 상태가 된다.
BOOL ReleaseMutex(
// 반환할 뮤텍스의 핸들을 인자로 전달한다.
// Non-Signaled 상태에 있는 뮤텍스는 Signaled 상태가 된다.
HANDLE hMutex
);
이제 기억할 사항은 WaitForSingleObject 함수의 특성이다. 이 함수는 인자로 전달된 핸들의 커널 오브젝트가 Signaled 상태가 되어 반환하는 경우, 해당 커널 오브젝트를 Non-Signaled 상태로 변경해버린다.
쓰레드(1) -> (입구) -> 임계 영역 -> (출구) -> 쓰레드(2)
(1) : WaitForSingleObject 흭득
(2) : ReleaseMutex 반납
위와 같은 구성이 가능하다. 쓰레드는 임계 영역에 들어가기 전, 뮤텍스를 흭득하고 뮤텍스 핸들을 인자로 전달하면서 WaitForSingleObject 함수를 호출한다. 만약 뮤텍스가 흭득 가능한 상태라면 Signaled 상태에 있을 것이고, 뮤텍스 흭득을 통해 임계 영역 진입에 가능하다.
이후, WaitForSingleObject 함수는 커널 오브젝트가 Signaled 상태가 되어 반환될 경우, 해당 커널 오브젝트의 상태를 Non-Signaled 상태로 변경하므로, 다른 쓰레드들은 임계 영역으로의 진입이 제한된다.
임계 영역에서 일을 마친 쓰레드는 ReleaseMutex 함수를 통해 다른 쓰레드의 진입을 허락한다. 참고로, 뮤텍스는 커널 오브젝트이므로 CloseHandle을 통해서 반환하면 된다.
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
#define NUM_OF_GATE 6
LONG gTotalCount = 0;
HANDLE hMutex;
void IncreaseCount()
{
WaitForSingleObject(hMutex, INFINITE);
gTotalCount++;
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];
hMutex = CreateMutex(
NULL,
FALSE,
NULL
);
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("totla count : %d\n"), gTotalCount);
for (DWORD i = 0; i < NUM_OF_GATE; i++)
{
CloseHandle(hThreads[i]);
}
CloseHandle(hMutex);
}
위 예제는 뮤텍스 관련 함수를 삽입하여 gTotalCount를 증가시키는 동작을 한다. 여기에서 사용되는 WaitForSingleObject 함수는 많은 용도로 사용된다. 때문에 함수를 Wrapping해서 사용하는 방법을 많이 추천한다. 최적화 과정에서 함수의 호출 문장을 함수의 몸체 부분으로 대치시키므로 성능의 저하는 일어나지 않는다.
04. B. 세마포어Semaphore 기반의 동기화
세마포어는 뮤텍스와 상당히 유사하다고 말한다. 'Modern Operating System'의 저자 Tanenbaum 교수의 말을 빌리면, '세마포어 중에서 단순화된 세마포어를 가리켜 뮤텍스라 한다'고 언급한다. 말을 정리하면, 뮤텍스는 세마포어의 일종이라고 볼 수 있기 때문에 유사하다는 말은 적절치 않을 수 있다.
세마포어와 뮤텍스는 이렇게 비슷한 취급을 받지만, 별개의 동기화 기법으로 설명되고 있는 점을 생각하면 차이점이 있다는 것을 알 수 있다. 바로 카운트 기능인데, 세마포어는 카운트 기능이 존재하는 반면에 뮤텍스에는 존재하지 않는다.
카운트 기능은 임계 영역에 동시에 접근할 수 있는 쓰레드 개수가 10개가 된다는 가정(혹은 유사한 환경)에서 사용 할 수 있다.
HANDLE CreateSemaphore (
// 보안 속성을 지정하기 위한 매개 변수다.
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
// 이 값을 기반으로 임계 영역에 접근 가능한 쓰레드의 개수를 제한한다.
LONG lInitialCount,
// 세마포어가 지닐 수 있는 값의 최대 크기를 지정한다.
// 값이 1일 경우, 뮤텍스와 동일한 기능을 하는 바이너리 세마포어가 구성된다.
// lInitialCount로 전달되는 값보다 커야 한다.
LONG IMaxiumCount,
// 세마포어에 이름을 붙일 때 사용된다.
LPCTSTR lpName
);
위 내용은 세마포어를 생성하는 함수의 정의다. 보이다시피, 세마포어는 값(카운트)을 가진다. 세마포어의 생성 시, lInitialCount에 의해 초기 카운트가 결정된다. 카운트가 0일 경우 Non-Signaled 상태에 놓이게 되며, 1 이상의 경우 Signaled 상태에 있게 된다.
세마포어의 핸들을 인자로 전달하며 WaitForSingleObject 함수를 호출할 경우, 그 값이 하나씩 감소하며 함수를 반환한다. 따라서 초기 카운트를 10로 설정할 경우, WaitForSingleObject 함수가 10번 호출되며, 카운트가 하나씩 감소한다. 이후, 11번째 호출에서는 블로킹 상태로 변경된다.
임계 영역을 빠져나온 쓰레드는 아래의 ReleaseSemaphore 함수를 호출해야 한다. 이 함수는 세마포어 카운트를 증가시키는 역할을 한다.
BOOL ReleaseSemaphore (
// 반환하려는 세마포어의 핸들
HANDLE hSemaphore,
// 세마포어 카운트의 증가값을 결정한다.
// 세마포어 카운트의 최대치를 넘기는 값을 넣을 경우, FALSE를 반환한다.
LONG IReleaseCount,
// 변경 전의 세마포어 카운트 값을 저장할 변수를 지정하며, 필요 없을 경우 NULL을 전달한다.
LPLONG lpPreviousCount
);
#include <stdio.h>
#include <tchar.h>
#include <time.h>
#include <Windows.h>
#include <process.h>
#define NUM_OF_CUSTOMER 50
#define RANGE_MIN 3
#define RANGE_MAX (30 - RANGE_MIN)
#define TABLE_CNT 3
HANDLE hSemaphore;
DWORD randTimeArr[50];
void TakeMeal(DWORD time)
{
WaitForSingleObject(hSemaphore, INFINITE);
_tprintf(_T("Enter Customer %d~ \n"), GetCurrentThreadId());
_tprintf(
_T("Customer %d \n"), GetCurrentThreadId()
);
Sleep(1000 * time);
ReleaseSemaphore(hSemaphore, 1, NULL);
_tprintf(_T("Out %d \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));
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
);
if (hSemaphore == NULL)
{
_tprintf(_T("CreateSemaphore error : %d\n"), GetLastError());
}
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;
}
04. C. 이름있는 뮤텍스Named Mutex 기반의 프로세스 동기화
뮤텍스에 이름을 붙이면 이름 있는 뮤텍스Named Mutex, 세마포어에 이름을 붙이면 이름있는 세마포어Named Semaphore라고 한다. 뮤텍스는 커널, 운영체제의 소유이다. 그렇기 때문에 프로세스 A 요청에서 생성이 되어도 프로세스 B에서도 접근이 가능하다.
단, 핸들의 유효성에 대한 문제가 있다. 핸들 테이블은 커널 오브젝트와 이를 지칭하는 핸들값에 대한 정보를 담고 있는 테이블인데, 이는 각각의 프로세스별로 독립적이다.
명칭 | 핸들 | 주소 | 상속여부 |
프로세스 A | 204 | 0x1200 | N |
프로세스 B | 정보 없음 | ... | ... |
위 표를 가지고 생각해보면, 프로세스 B에서는 프로세스 A에서 생성한 뮤텍스에 대한 정보가 없기 때문에 접근이 불가능하다. 이럴 경우, 이름있는 뮤텍스가 사용된다.
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
HANDLE hMutex;
DWORD dwWaitResult;
void ProcessBaseCriticalSection()
{
dwWaitResult = WaitForSingleObject(hMutex, INFINITE);
switch (dwWaitResult)
{
case WAIT_OBJECT_0:
_tprintf(_T("Threa Got Mutex! \n"));
break;
case WAIT_TIMEOUT:
_tprintf(_T("Timer Expired ! \n"));
break;
case WAIT_ABANDONED:
return;
}
for (DWORD i = 0; i < 5; i++)
{
_tprintf(_T("Timer Running! \n"));
Sleep(10000);
}
ReleaseMutex(hMutex);
}
int _tmain(int argc, TCHAR* argv[])
{
#if 0 // 이 부분
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;
}
위 예제를 통해서 이름있는 뮤텍스가 다른 프로세스에서 공유될 수 있다는 것을 볼 수 있다. 주석에 표시되어 있는 부분을 0과 1로 설정하여 각각 빌드를 한 후, 실행을 해보면 하나의 프로세스가 종료된 이후, 다음 프로세스가 이어서 실행되는 것을 볼 수 있다.
HANDLE OpenMutex (
// 접근 권한을 지정한다.
// MUTEX_ALL_ACCESS를 전달하여 접근할 수 있는 권한을 요청해야 한다.
DWORD dwDesiredAccess,
// 핸들의 상속 유무를 결정한다.
BOOL bInheritHandle,
// 얻고자 하는 핸들 정보의 커널 오브젝트 이름을 전달한다.
// 전달하는 이름과 일치하는 이름의 뮤텍스가 존재할 경우, 해당 뮤텍스의 핸들이 반환된다.
// 추가적적으로 핸들 테이블 정보도 추가된다.
LPCTSTR lpName
);
04. D. 뮤텍스의 소유와 WAIT_ABANDONED
WaitForSingleObject 함수의 반환값으로 WAIT_ABANDONED가 있다. 설명을 위해서 A, B 쓰레드가 있고 세마포어 오브젝트 C가 있다고 가정하자. A 쓰레드는 세마포어 C의 카운트를 하나 감소시켰다. 그러면, 다시 A 쓰레드는 카운트를 증가시켜야 한다. 이것이 일반적인 멀티 쓰레드 동기화 구조다. 그러나, 이런 제약사항을 반드시 지켜야하는 것은 뮤텍스 하나이다.
세마포어는 뮤텍스와 달리, 세마포어를 흭득하는 쓰레드와 반환하는 쓰레드가 달라도 문제되지 않는다. 뮤텍스를 흭득한 쓰레드가 반환하지 못하고 예상치 못하게 종료되었을 경우, Windows는 이러한 상황을 파악해서 반환이 불가능한 뮤텍스를 대신 반환하고, 다음 대기중인 쓰레드에 뮤텍스를 전달한다.
이때, 다음 대기중인 쓰레드는 WAIT_ABANDONED 값을 반환받게 된다. 이러한 개념을 가졌기 때문에 실제로 활용하는 경우보다는 디버깅에 사용되는 경우가 일반적이다.
'독서 > 뇌를 자극하는 윈도우즈 시스템 프로그래밍' 카테고리의 다른 글
[스터디] 시스템 프로그래밍 - Chapter 15. 쓰레드 풀링Pooling (1) | 2023.11.24 |
---|---|
[스터디] 시스템 프로그래밍 - Chapter 14. 쓰레드 동기화 기법 2 (1) | 2023.11.08 |
[스터디] 시스템 프로그래밍 - Chapter 12. 쓰레드의 생성과 소멸 (2) | 2023.11.01 |
[스터디] 시스템 프로그래밍 - Chapter 11. 쓰레드의 이해 (3) | 2023.10.30 |
[스터디] 시스템 프로그래밍 - Chapter 10. 컴퓨터 구조에 대한 세 번째 이야기 (2) | 2023.10.25 |