윈도우즈 시스템 프로그래밍 14장 - 실행순서에 있어서의 동기화 / 이벤트 더하기 뮤텍스
윈도우즈 시스템 프로그래밍이라는 책과 해당 책의 저자이신 윤성우님의 강의를 통해 공부한 내용을 정리하는 글입니다.
생산자/소비자 모델
빵을 굽는 생산자는 구운 빵을 테이블 위에 올려놓는다. 그러면 값을 지불한 소비자는 테이블에 올려진 빵을 먹게 된다. 이때, 순서가 중요하다. 생산자가 빵을 테이블 위에 올려놓는 것이 먼저다. 그런 다음에 소비자가 빵을 소비할 수 있다. 만일 순서가 바뀐다면 소비자는 빈 테이블에서 빵을 찾게 될 것이고, 소비자가 빈 테이블임을 알고 떠난 뒤에야 생산자가 빵을 가져다 놓게 된다. 그렇다면 빵은 소비되지 않은 채 남아있게 된다.
이 모델을 프로그램에 적용시키면, 생산자는 문자열을 생성한다(입력받는다), 소비자는 생산된 문자열을 소비한다(출력한다) 문자열을 입력받는 것은 입출력 연산 중에서 입력에 해당되고, 문자열을 출력하는 것은 출력에 해당된다.
만약 하나의 쓰레드가 문자열을 입력받고나서 출력도 하도록 구현되어 있다면, 출력속도가 입력속도를 따라가지 못하는 문제가 발생할 수 있다.
이를 해결하기 위하여 두 개의 쓰레드 중 하나는 입력을, 하나는 출력을 담당하게 하여 그 사이에 메모리 버퍼를 통해 입출력 속도에 상관없이 독립적으로 실행되도록 할 수 있다.
이벤트(Event) 기반 동기화
생산자가 빵을 구워낸 상태가 되면, 이 상태를 감지한 소비자가 빵을 소비하게 된다. 상태에 따라서 실행되어야 할 쓰레드가 결정된다.
⦁ 이벤트 오브젝트를 생성하는 데 사용되는 함수 CreateEvent
HANDLE CreateEvnet (
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);
// If the function fails, the return value is NULL.
⦁ lpEventAttributes : 보안 속성 지정. 핸들 상속에 사용된다.
⦁ bManualReset : 수동 리셋모드로 이벤트 오브젝트를 생성(TRUE)할지, 자동 리셋 모드로 생성(FALSE)할지 결정.
⦁ bInitialState : 이벤트 오브젝트의 초기 상태. TRUE 전달되면 Signaled 상태로 생성, FALSE가 전달되면 Non-Signaled 상태로 생성된다.
⦁ lpName : NULL을 전달하면 이름 없는 이벤트 오브젝트가 생성된다.
이벤트 오브젝트를 소멸시킬 때는 CloseHandle을 사용하면 된다.
⦁ 이벤트 커널 오브젝트는 프로그래머의 요청에 의해 Signaled 상태가 된다.
⦁ Non-Signaled 상태의 이벤트 오브젝트 때문에 WaitForSingleObject 함수 호출이 블로킹되었다면, Signaled 상태가 되는 순간 블로킹된 함수를 빠져나오게 된다. 그리고 이때 자동 리셋 모드 이벤트 오브젝트라면, Non-Signaled 상태로의 변경은 자동으로 이루어진다.
⦁ 수동 리셋 모드 이벤트는 Signaled 상태에서 Non-Signaled로의 상태 변경을 위해 ResetEvent 함수를 호출해야 한다.
⦁ 자동 리셋 모드 이벤트는 WaitForSingleObject 함수를 반환하면서 자동으로 Non-Signaled 상태로 돌아간다.
⦁ ResetEvent 함수
BOOL ResetEvent (
HANDLE hEvent;
);
// If the function fails, the return value is zero.
⦁ hEvent : 이벤트 오브젝트의 핸들을 전달. 전달한 핸들의 오브젝트는 Non-Signaled 상태가 된다.
⦁ SetEvent
BOOL SetEvent (
HANDLE hEvent
);
// If the function fails, the return value is zero.
⦁ hEvent : 이벤트 오브젝트의 핸들을 전달. 전달된 핸들의 오브젝트는 Signaled 상태가 된다.
⦁ 문자열 관련 생산자/소비자 예제 StringEvent.cpp
#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;
}
문자열이 입력될 때까지 문자열을 출력하는 쓰레드가 블로킹 상태이다.
수동 리셋(Manual-Reset)모드 이벤트(Event)의 활용 예 StringEvent2.cpp
이전 예제는 자동 리셋 모드로 생성해도 문제없는 상황이었다.
이번 예제는 입력받은 문자열의 길이를 계산하는 역할의 쓰레드도 추가할 것이다. 이 역시 소비자 쓰레드이다.
#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);
_fputts(string, stdout);
return 0;
}
unsigned int WINAPI CountThreadFunction(LPVOID lpParam)
{
WaitForSingleObject(hEvent, INFINITE);
_tprintf(_T("output string length: %d \n"), _tcslen(string) - 1);
return 0;
}
문자열을 입력받은 쓰레드가 이벤트를 Signaled 로 변경하면 두 개의 소비자 쓰레드가 동시에 블로킹 상태를 빠져 나와 실행을 재개하므로 기대한 형태로 출력이 되지 않을수도 있다.
이벤트와 뮤텍스 오브젝트 적용 예제
⦁ StringEvent3.cpp
#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 hThreads[2];
DWORD dwThreadIDs[2];
gSynString.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
gSynString.hMutex = CreateMutex(NULL, FALSE, NULL);
if (gSynString.hEvent == NULL || gSynString.hMutex == NULL)
{
_fputts(_T("kernel object creation error \n"), stdout);
return -1;
}
hThreads[0] = (HANDLE)_beginthreadex(NULL, 0, OutputThreadFunction, NULL, 0, (unsigned*)&dwThreadIDs[0]);
hThreads[1] = (HANDLE)_beginthreadex(NULL, 0, CountThreadFunction, NULL, 0, (unsigned*)&dwThreadIDs[1]);
if (hThreads[0] == NULL || hThreads[1] == NULL)
{
_fputts(_T("Thread creation error \n"), stdout);
return -1;
}
_fputts(_T("Insert string: "), stdout);
_fgetts(gSynString.string, 30, stdin);
SetEvent(gSynString.hEvent);
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
CloseHandle(gSynString.hEvent);
CloseHandle(gSynString.hMutex);
CloseHandle(hThreads[0]);
CloseHandle(hThreads[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("OUtput string length: %d \n"), _tcslen(gSynString.string) - 1);
ReleaseMutex(gSynString.hMutex);
return 0;
}