뇌를 자극하는 윈도우즈 시스템 프로그래밍(저자, 윤성우)
01. 실행순서에 있어서의 동기화
쓰레드의 실행순서를 동기화한다는 것은 메모리에 접근하는 쓰레드의 실행순서를 동기화한다는 것과 같은 이야기다. 그러므로, 실행순서 동기화는 메모리 접근 동기화를 포함하는 개념이다.
01. A. 생산자/소비자 모델
생산자/소비자 모델을 예시를 통해서 이해해보자. 빵을 구워내는 생산자가 있다. 생산자는 빵을 테이블 위에 올려두고, 소비자는 생산된 빵을 구매하고 먹게된다. 해당 예시에서의 순서는 생산자가 빵을 테이블 위에 올리고, 그 이후에 소비자가 빵을 소비한다는 것이다.
생산자/소비자 모델이 필요한 예시로 문자를 입력받고 문자를 출력하는 기능을 하나의 쓰레드에서 구현을 처리한다고 들어보자. 만약 이럴 경우, 출력속도가 입력속도를 따라가지 못하는 상황이 발생할 수 잇다.
입력과 출력의 비율이 일치하다가 어느 순간, 입력의 양이 많아지는 상황이다. 이러한 경우 시스템은 입력되는 문자열을 감당하지 못하고 어느 순간에는 문자열이 소멸된다.
이 때, 모델을 적용하여 두 개의 쓰레드를 활용하여 하나는 입력, 하나는 출력을 담당토록 한다. 그 사이에 메모리 버퍼를 두어서 두 개의 쓰레드가 입력 및 출력 속도에 상관 없이 독립적으로 실행되도록 한다.
01. B. 이벤트 기반 동기화
세마포어나 뮤텍스와 같이 이 기법에서도 동기화를 위한 오브젝트, 이벤트 오브젝트가 필요하다. 보편적으로는 이벤트라고 말한다. 필요한 함수들은 다음과 같다.
HANDLE CreateEvent (
// 보안 속성을 지정할 때 사용된다.
LPSECURITY_ATTRIBUTES lpEventAttributes,
// 리셋 모드를 지정한다.
// TRUE : 수동 리셋 모드 생성
// FALSE : 자동 리셋 모드 생성
BOOL bManualReset,
// 초기 상태를 결정한다
// TRUE : Signaled 상태의 이벤트 오브젝트 생성
// FALSE : Non-Signaled 상태의 이벤트 오브젝트 생성
BOOL bInitialState,
// 이벤트 오브젝트의 이름을 지정한다.
// NULL : 이름 없음
LPCSTR lpName
);
BOOL ResetEvent (
// 이벤트 오브젝트 핸들을 인자로 전달한다.
// 전달된 핸들의 오브젝트는 Non-Signaled 상태가 된다.
HANDLE hEvent
);
BOOL SetEvent(
// 이벤트 오브젝트의 핸들을 인자로 전달한다.
// 전달된 핸들의 오브젝트는 Signaled 상태가 된다.
HANDLE hEvent
);
이벤트 오브젝트의 소멸이 필요하다면, CloseHandle 함수를 통해서 처리할 수 있다. 이제, 이벤트 오브젝트의 활용을 보자.
쓰레드나 커널 오브젝트의 경우, 초기에는 Non-Signaled 상태로 생성된다. 쓰레드나 프로세스 종료 시에는 Signaled 상태로 자동 변경된다. 그렇지만, 이벤트 오브젝트는 자동으로 Signaled 상태가 되지 않는다. 자동으로 Signaled 상태가 되는 특정 상황이 존재하지 않는다. 그래서 위의 ResetEvent 함수와 SetEvent 함수를 통해서 Signaled 상태와 Non-Signaled 상태를 변환해준다.
Non-Signaled 상태의 이벤트 오브젝트 핸들을 인자로 전달하면, WaitForSingleObject 함수를 호출한 쓰레드는 블로킹 생태가 된다. Signaled 상태가 되면, WaitForSingleObject 함수를 반환한다. 이벤트 오브젝트가 Signaled 상태가 되어 블로킹 상태에 있던 쓰레드가 빠져 나온다면, 이벤트 오브젝트가 놓이는 상태에 따라 수동 리셋 모드, 자동 리셋 모드를 구분할 수 있다.
Signaled 상태 그대로일 경우에는 수동 리셋 모드, Non-Signaled 상태로 변경되었다면 자동 리셋 모드의 이벤트 오브젝트라는 의미가 된다.
이때, 수동 리셋 모드에서 WaitForSingleObject 함수가 필요 없다는 것이 아니라, 빠져 나오고도 Non-Siganeld 상태로 변경되지 않으므로, ResetEvent 함수를 통해 변경해줘야 함을 말하는 것이다.
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam);
TCHAR string[100];
HANDLE hEvent;
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hThread;
DWORD dwThreadID;
hEvent = CreateEvent(
NULL, TRUE, FALSE, NULL
);
if (hEvent == NULL)
{
_fputts(_T("Event Object Creation Error \n"), stdout);
return -1;
}
hThread = (HANDLE)_beginthreadex(
NULL, 0,
OutputThreadFunction,
NULL, 0,
(unsigned*)&dwThreadID
);
if (hThread == 0)
{
_fputts(_T("Thread Creation Error \n"), stdout);
return -1;
}
_fputts(_T("Insert String : "), stdout);
_fgetts(string, 30, stdin);
SetEvent(hEvent);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hEvent);
CloseHandle(hThread);
return 0;
}
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam)
{
WaitForSingleObject(hEvent, INFINITE);
_fputts(_T("Output string : "), stdout);
_fputts(string, stdout);
return 0;
}
01. C. 수동 리셋Manual-Reset 모드 이벤트의 활용 예
수동 리셋 모드와 자동 리셋 모드, 둘 중 아무거나로 생성해도 상관이 없다면 프로그래머의 취향에 따라 생성을 결정할 수 있는 문제다. 두 개의 모드를 취향이 아니라, 알맞는 상황에 따라 적절히 설정할 수 있도록, 수동 리셋 모드가 필요한 상황을 찾아보자.
앞선 예제에서는 두 개의 쓰레드가 각각 데이터를 입력, 그리고 출력하도록 역할이 분담되어 있었다. 여기에 하나의 쓰레드를 더 추가해서 소비자 역할을 맡겨보자. 구체적으로 입력받은 문자열의 길이를 계산해서 출력하는 역할을 맡을 것이다.
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam);
unsigned int WINAPI CountThreadFunction(LPVOID lpParam);
TCHAR string[100];
HANDLE hEvent;
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hThread[2];
DWORD dwThreadID[2];
hEvent = CreateEvent(
NULL, TRUE, FALSE, NULL
);
if (hEvent == NULL)
{
_fputts(_T("Event Object creation error \n"), stdout);
return -1;
}
hThread[0] = (HANDLE)_beginthreadex(
NULL, 0, OutputThreadFunction, NULL, 0, (unsigned*)&dwThreadID[0]
);
hThread[1] = (HANDLE)_beginthreadex(
NULL, 0, CountThreadFunction, NULL, 0, (unsigned*)&dwThreadID[1]
);
if (hThread[0] == 0 || hThread[1] == 0)
{
_fputts(_T("Thread Creation Error \n"), stdout);
return -1;
}
_fputts(_T("Insert string :"), stdout);
_fgetts(string, 30, stdin);
SetEvent(hEvent);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hEvent);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
return 0;
}
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam)
{
WaitForSingleObject(hEvent, INFINITE);
_fputts(_T("output string: "), stdout); // 1
_fputts(string, stdout); // 2
return 0;
}
unsigned int WINAPI CountThreadFunction(LPVOID lpParam)
{
WaitForSingleObject(hEvent, INFINITE);
_tprintf(_T("ouput string length: %d \n"), _tcslen(string) - 1); // 3
return 0;
}
위 예제에서 정상적인 출력과 비정상적인 출력은 다음과 같다.
// 정상
Insert string : I Love You
Ouput string : I Love You
Output string Length : 10
// 비정상
Insert string : I Love You
Ouput string : Output string Length : 10
I Love You
비정상적으로 출력이 되는 경우는 문자열을 입력받은 생산자 쓰레드가 이벤트 오브젝트를 Signaled 상태로 변경하면, 두 개의 소비자 쓰레드는 동시에 블로킹 상태를 빠져나와 실행을 재개하기 때문이다.
이때, 위 예제에서 주석으로 표시되어 있는 부분들은 임계영역은 아니다. 둘 이상의 쓰레드에 의해서 접근되는 영역이 아니기 때문이다. 그렇다고 생산자/소비자 모델에 부합되는 것도 아니다. 둘 다 소비자 역할의 쓰레드가 호출되는 함수이기 때문이다.
멀티 쓰레드 기반의 프로그래밍을 하다 보면, 교과서적인 모델로 설명하기 애매한 상황을 종종 접할 수 있기에 이론적인 모델에 얽매이지 않는 것이 중요하다.
02. 이벤트 + 뮤텍스
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam);
unsigned int WINAPI CountThreadFunction(LPVOID lpParam);
typedef struct _SynchString {
TCHAR string[100];
HANDLE hEvent;
HANDLE hMutex;
} SynchString;
SynchString gSynString;
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hThread[2];
DWORD dwThreadID[2];
gSynString.hEvent = CreateEvent(
NULL, TRUE, FALSE, NULL
);
gSynString.hMutex= CreateMutex(
NULL, FALSE, NULL
);
if (gSynString.hEvent == NULL || gSynString.hMutex == NULL)
{
_fputts(_T("Event Object creation error \n"), stdout);
return -1;
}
hThread[0] = (HANDLE)_beginthreadex(
NULL, 0, OutputThreadFunction, NULL, 0, (unsigned*)&dwThreadID[0]
);
hThread[1] = (HANDLE)_beginthreadex(
NULL, 0, CountThreadFunction, NULL, 0, (unsigned*)&dwThreadID[1]
);
if (hThread[0] == 0 || hThread[1] == 0)
{
_fputts(_T("Thread Creation Error \n"), stdout);
return -1;
}
_fputts(_T("Insert string :"), stdout);
_fgetts(gSynString.string, 30, stdin);
SetEvent(gSynString.hEvent);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(gSynString.hEvent);
CloseHandle(gSynString.hMutex);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
return 0;
}
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam)
{
WaitForSingleObject(gSynString.hEvent, INFINITE);
WaitForSingleObject(gSynString.hMutex, INFINITE);
_fputts(_T("output string: "), stdout);
_fputts(gSynString.string, stdout);
ReleaseMutex(gSynString.hMutex);
return 0;
}
unsigned int WINAPI CountThreadFunction(LPVOID lpParam)
{
WaitForSingleObject(gSynString.hEvent, INFINITE);
WaitForSingleObject(gSynString.hMutex, INFINITE);
_tprintf(_T("ouput string length: %d \n"), _tcslen(gSynString.string) - 1);
ReleaseMutex(gSynString.hMutex);
return 0;
}
앞선 예제의 경우 위와 같이 뮤텍스를 추가적으로 더함으로서 문제를 해결할 수 있다.
03. 타이머 기반 동기화
Windows에서는 Signaled 상태라는 개념이 중요하다. 어떠한 커널 오브젝트는 특정 상황에서 Signaled 상태가 되고, 어떤 커널 오브젝트는 명시적인 함수 호출을 통해서 Signaled 상태가 되기도 한다. 이번에는 정해진 시간이 지나면 자동으로 Signaled 상태가 되는 특성을 지니는 타이머Waitable Timer를 알아보자.
타이머 기반 쓰레드 동기화는 임계 영역 문제 해결을 위한 동기화와는 관점이 다르다. 쓰레드의 실행 시간 및 실행 주기를 결정하겠다는 의미이기 때문이다. 타이머 기반 동기화는 다음과 같이 두 가지 형태로 구분 지을 수 있다.
1. 수동 리셋 타이머 : 가장 일반적인 타이머이며, 맞춰둔 시간에 따라 발동한다.
2. 주기적 타이머 : 수동 리셋 타이머에 주기적인 특성이 더해졌다고 이해하면 된다.
03. A. 수동 리셋 타이머
타이머 오브젝트는 정해진 시간이 지나야 Signaled 상태가 되는 커널 오브젝트다. 이에 필요한 함수들은 다음과 같다.
HANDLE CreateWaitableTimer (
// 보안 속성을 지정한다.
LPSECURITY_ATTRIBUTES lpTimerAttributes,
// 타이머의 모드를 지정한다.
BOOL bManualReset,
// 타이머 오브젝트에 이름을 붙인다.
// NULL : 이름 없는 타이머 오브젝트가 생성된다.
LPCTSTR lpTimerName
);
BOOL SetWaitableTimer (
// 알람을 설정할 타이머 오브젝트의 핸들을 전달한다.
HANDLE hTimer,
// 알람이 울리는 시간을 지정하며, 100만 분의 1초 단위로 시간을 설정한다.
// + : 절대 시간을 의미한다.
// - : 상대 시간을 의미한다.
const LARGE_INTEGER* pDueTime,
// 타이머가 주기적으로 알람을 울리게 할 때 사용한다.
// 시간 간격을 1/1000초로 전달하면 된다.
// 0 : 주기적인 알람을 사용하지 않는다.
LONG lPeriod,
// 완료 루틴 타이머를 생성하는 용도
PTIMERAPCROUTINE pfnCompletionRoutine,
// 완료 루틴 타이머를 생성하는 용도
LPVOID lpArgToCompletionRoutine,
// 전원 관리와 관련있는 매개 변수이다.
BOOL fResume
);
예제를 통해서 타이머 오브젝트를 사용해보자.
#define _WIN32_WINNT 0x0400 // (1)
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hTimer = NULL;
LARGE_INTEGER liDueTime; // (2)
liDueTime.QuadPart = -100000000;
hTimer = CreateWaitableTimer(NULL, FALSE, _T("WaitableTimer"));
if (!hTimer)
{
_tprintf(_T("CreateWaitableTimer Failed (%d)", GetLastError()));
return -1;
}
_tprintf(_T("Wating for 10 seconds... \n"));
SetWaitableTimer(hTimer, &liDueTime, 0, NULL, NULL, FALSE);
WaitForSingleObject(hTimer, INFINITE);
_tprintf(_T("Timerr was signaled. \n"));
MessageBeep(MB_ICONEXCLAMATION);
return 0;
}
위 예제 코드에서 주석 중, (1)에 해당하는 부분은 Windows 버전이 증가함에 따라서 생긴 새로운 기능, CreateWaitableTimer와 SetWaitableTimer를 사용하기 위해 추가한다. Windows XP 이후 버전에서부터 제공하기 시작한 함수는 매크로 _WINNT_WIN32를 0x0501으로 정의해야 한다.
(2)에 해당하는 부분은 64비트 정수를 표현하지 못하는 시스템에서 64 비트 정수를 표현하기 위해 선언된 자료형으로 원형은 다음과 같다.
typedef union _LARGE_INTEGER
{
struct {
DWORD LowPart;
LONG HighPart;
};
LONGLONG QuadPart;
} LARGE_INTEGER, *PLARGE_INTEGER;
64비트 정수형 데이터를 표현하고자 하는 경우, 위에서 처럼 LONGLONG(_int64)로 선언된 멤버 QuadPart를 활용할 수 있다.
03. B. 주기적 타이머Periodic-Timer
주기적 타이머는 일정 시간 간격으로 알람이 울리는 타이머를 의미한다. 아래 예제는 5초마다 한 번 소리가 들리는 동작을 수행한다.
#define _WIN32_WINNT 0x0400
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hTimer = NULL;
LARGE_INTEGER liDueTime;
liDueTime.QuadPart = -100000000;
hTimer = CreateWaitableTimer(NULL, TRUE, _T("WaitableTimer"));
if (!hTimer)
{
_tprintf(_T("CreateWaitableTimer Failed (%d)", GetLastError()));
return -1;
}
_tprintf(_T("Wating for 10 seconds... \n"));
SetWaitableTimer(hTimer, &liDueTime, 5000, NULL, NULL, FALSE);
while (1)
{
WaitForSingleObject(hTimer, INFINITE);
_tprintf(_T("Timerr was signaled. \n"));
MessageBeep(MB_ICONEXCLAMATION);
}
return 0;
}
중간에 타이머를 중지시키고자 한다면, 아래의 함수를 통해 할 수 있다. 이때, 이는 단순히 타이머를 중지시키는 것이지 자원의 반환으로 이루어지지 않기 때문에 반환을 하고자 한다면, CloseHandle 함수를 통해 할 수 있다.
BOOL CancelWaitableTimer (
// 알람을 해제할 타이머 오브젝트의 핸들을 전달한다.
HANDLE hTimer
);
'독서 > 뇌를 자극하는 윈도우즈 시스템 프로그래밍' 카테고리의 다른 글
[스터디] 시스템 프로그래밍 - Chapter 16. 컴퓨터 구조의 4번째 이야기 (1) | 2023.11.28 |
---|---|
[스터디] 시스템 프로그래밍 - Chapter 15. 쓰레드 풀링Pooling (1) | 2023.11.24 |
[스터디] 시스템 프로그래밍 - Chapter 13. 쓰레드 동기화 기법 1 (1) | 2023.11.08 |
[스터디] 시스템 프로그래밍 - Chapter 12. 쓰레드의 생성과 소멸 (2) | 2023.11.01 |
[스터디] 시스템 프로그래밍 - Chapter 11. 쓰레드의 이해 (3) | 2023.10.30 |