실행 순서 동기화 = 둘 이상의 쓰레드의 실행 순서를 조정.
아래 이미지의 모델은 I/O 모델 or 쓰레드 모델
입력 = 콘솔 입력, 외부에서 들어오는 입력..
출력 = 들어온 입력 데이터를 가공해 이용
입력 - 출력을 번갈아서 실행.
출력의 경우 입력의 크기에 의존적이고 입력의 경우 외부에 의존적. 10byte의 크기까지 한 번에 처리할 수 있는 시스템에 10MB의 데이터가 외부에서 한 번에 들어온다면 출력 시 데이터 손실이 발생할 수 있음.
따라서 보통 아래 이미지와 같이 입력/출력 쓰레드와 출력용 버퍼를 두는 형태로 I/O 모델이 디자인됨.
입력 쓰레드는 데이터의 크기가 어떻든 출력용 버퍼에 데이터를 쌓아둠. 이 경우 버퍼가 견디는 한도에서는 출력 쓰레드가 데이터를 가져가는 속도가 느리더라도 문제가 생기지 않음.
이 때 입력 쓰레드 = 생산자. 데이터를 제공
출력 쓰레드 = 소비자. 제공된 데이터를 소비
중요한 점은 생산자의 실행이 완료된 다음에 소비자가 실행을 진행해야 함.
만약 순서가 잘못될 경우, 다음과 같은 문제가 생길 수 있음.
현재 버퍼가 비어있는 상태에서 생산자가 데이터를 가져다두기도 전에 소비자가 데이터를 가져간다면, 해당 데이터는 의미가 없는 쓰레기 값. 따라서 실행 순서의 동기화가 필요하다.

1) 생산자 쓰레드가 버퍼에 데이터를 가져다두면 소비자 쓰레드는 데이터를 가져간다는 쓰레드 간의 약속이 필요. 이를 위해 생산자 쓰레드가 실행을 완료했다는 사실을 소비자 쓰레드가 알아야 하는데, 해당 사실은 생성자 쓰레드 본인만 알고 있음.
2) 따라서 두 쓰레드는 CreateEvent() 함수를 통해 초기 상태가 Non-Signaled 상태인 커널(이벤트) 오브젝트를 만듦. 생산자 쓰레드는 자기가 할 일을 마치고 소비자 쓰레드가 진입해야 할 시점에 커널 오브젝트를 Signaled 상태로 만들기로 약속.
3) 소비자 쓰레드는 WaitForSingleObejct() 함수를 통해 커널 오브젝트를 관찰. 이 함수를 호출하면 소비자 쓰레드는 Blocked 상태에 빠짐.
4) 생산자 쓰레드는 자신의 할 일을 마치면 SetEvent() 함수를 통해 커널 오브젝트를 Signaled 상태로 만듦.
CreateEvent() 함수를 통해 이벤트 오브젝트를 생성. 이 때 이벤트 모드를 정할 수 있음.
1) 수동 리셋 모드 이벤트
이벤트 오브젝트의 상태를 수동으로 조작해줘야 하는 오브젝트.
2) 자동 리셋 모드 이벤트
SetEvent() 함수를 통해 이벤트를 주는 것, 즉 커널 오브젝트를 Signaled 상태로 만드는 건 여전히 수동. 이는 생산자 쓰레드만이 그 타이밍을 알 수 있기 때문.
그러나 이벤트 오브젝트가 Signaled 상태에서 Non-Signaled 상태가 되는 건 자동.
다시 말해 WaitForSingleObject() 함수를 호출해 쓰레드가 Blocked 상태에 있다가 Signaled 상태가 되면 쓰레드가 함수를 빠져나오는데, 이와 동시에 이벤트 오브젝트가 Non-Signaled 상태가 됨.
자동 리셋 모드 이벤트 오브젝트와 수동 리셋 모드 이벤트 오브젝트가 있고, 각 오브젝트에는 쓰레드 A와 쓰레드 B가 대기 중에 있다고 가정.
이 때 생산자가 쓰레드가 자동 리셋 모드 오브젝트의 상태를 Signaled 상태로 바꿈. 이 경우 WaitForSingleObject() 함수를 호출해 대기 중인 두 쓰레드 중 한 쓰레드만 빠져나온 뒤 자동으로 Non-Signaled 상태가 됨. 즉 둘 중 하나의 쓰레드만 실행의 기회를 얻는다.
반면 수동 리셋 모드 오브젝트의 경우 이벤트 오브젝트가 계속 Signaled 상태로 남아있기 때문에 두 쓰레드가 동시에 빠져나오게 될 것.
결론은 한 순간에 하나의 쓰레드만 깨어나게 하고 싶은 경우 자동 리셋 모드 이벤트를 사용. 관찰하고 있던 둘 이상의 쓰레드를 동시에 깨어나게 하려면 수동 리셋 모드 이벤트를 사용.

/*
StringEvent.cpp
프로그램 설명: 1. 생산자/소비자 모델의 이해
2. 동기화 event에 대한 이해.
3. 메인 쓰레드 - 자식 쓰레드 사이의 동기화
4. 메인 쓰레드가 생산자 쓰레드, 자식 쓰레드가 소비자 쓰레드
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h> /* _beginthreadex, _endthreadex */
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam);
TCHAR string[100];
HANDLE hEvent;
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hThread;
DWORD dwThreadID;
hEvent = CreateEvent( // event object 생성.
NULL, // 상속 불가.
TRUE, // 수동 리셋 모드(manual-reset mode)로 생성.
FALSE, // non-signaled 상태로 생성.
NULL // 이름 없는 event.
);
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); // [메인 쓰레드]event의 state를 signaled 상태로 변경.
WaitForSingleObject(hThread, INFINITE); // [메인 쓰레드] 메인 쓰레드가 먼저 종료되면 안되기에 기다림.
CloseHandle(hEvent); // event 오브젝트 소멸
CloseHandle(hThread);
return 0;
}
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam)
{
WaitForSingleObject(hEvent, INFINITE); // [자식 쓰레드]event가 signaled 상태가 되기를 기다린다.
_fputts(_T("output string: "), stdout);
_fputts(string, stdout);
return 0;
}
아래 예제에서는 두 쓰레드가 하나의 이벤트 오브젝트를 관찰하고 있는 상황. 따라서 두 쓰레드가 동시에 실행될 수 있다. 임계 영역은 이전까지 메모리 접근의 문제점과 관련이 있는 하나의 코드 블럭이라고 설명됨. 그러나 아래 예제에서는 코드 블럭이 분리되어 있지만 두 쓰레드가 동시가 실행되어 출력이 섞여 나올 수 있는 문제가 생김. 즉, 실행 순서 동기화는 되었지만 콘솔에 접근하는 건 동기화가 되지 않은 상태.
이러한 경우 콘솔에 접근하는 걸 동기화해주거나, 실행 순서를 동기화한다는 두 가지 방안이 있음.
결론은 임계 영역이라는 것은 하나의 코드 블럭뿐 아니라, 둘 이상의 코드 블럭을 의미할 수도 있다.
/*
StringEvent2.cpp
프로그램 설명: manual-reset mode 동기화 적용 사례.
이전 예제와 다르게, 하나의 쓰레드가 더 등장.
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h> /* _beginthreadex, _endthreadex */
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( // event object 생성.
NULL, // 상속 불가.
TRUE, // manual-reset mode로 생성.
FALSE, // non-signaled 상태로 생성.
NULL // 이름 없는 event.
);
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); // event의 state를 signaled 상태로 변경.
WaitForMultipleObjects ( // [메인 쓰레드] 둘 이상의 쓰레드를 기다릴 때 호출하는 함수.
2, // 배열의 길이.
hThread, // 핸들의 배열.
TRUE, // 모든 핸들이 신호받은 상태로 될 때 리턴.
INFINITE // 무한 대기.
);
CloseHandle(hEvent); // event 오브젝트 소멸
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
return 0;
}
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam)
{
WaitForSingleObject(hEvent, INFINITE); // event가 signaled 상태가 되기를 기다린다.
_fputts(_T("Output string: "), stdout);
_fputts(string, stdout);
return 0;
}
unsigned int WINAPI CountThreadFunction(LPVOID lpParam)
{
WaitForSingleObject(hEvent, INFINITE); // event가 signaled 상태가 되기를 기다린다.
_tprintf(_T("Output string length: %d \n"), _tcslen(string)-1);
return 0;
}
/*
StringEvent3.cpp
프로그램 설명: event, mutex 동시 사용 사례.
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h> /* _beginthreadex, _endthreadex */
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]==0 ||hThreads[1]==0)
{
_fputts(_T("Thread creation error \n"), stdout);
return -1;
}
_fputts(_T("Insert string: "), stdout);
_fgetts(gSynString.string, 30, stdin);
SetEvent(gSynString.hEvent); // event의 state를 signaled 상태로 변경.
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); // event가 signaled 상태가 되기를 기다린다.
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); // event가 signaled 상태가 되기를 기다린다.
WaitForSingleObject(gSynString.hMutex, INFINITE); // 뮤텍스 추가
_tprintf(_T("Output string length: %d \n"), _tcslen(gSynString.string)-1);
ReleaseMutex(gSynString.hMutex);
return 0;
}
소프트웨어 상 타이머와 이벤트 오브젝트가 어떤 의미를 지니는지에 대한 내용.
타이머도 커널 오브젝트. 타이머가 알람을 울린다는 건 해당 커널 오브젝트가 Signaled 상태가 되었다는 걸 의미함.
수동 리셋 타이머는 Signaled 상태가 되는 시간을 수동으로 지정. 10초 뒤 알람을 울리는 기능.
자동 리셋 타이머는 수동 리셋 타이머 + 기능을 지님. 즉, 10초 뒤 알람을 울리는 기능 + 이후 매 5초 간격으로 알람을 울리는 것이 가능. 이는 알람을 울린 후(Signaled 상태가 된 후) 자동으로 Non-Signaled 상태가 된다는 것 또한 의미.
HANDLE CreateWaitableTimer( // 타이머 커널 오브젝트 생성.
LPSECURITY_ATTRIBUTES lpTimerAttributes,
BOOL bManualReset,
LPCTSTR lpTimerName
)
BOOL SetWaitableTimer( // 타이머 커널 오브젝트의 시간 설정.
HANDLE hTimer,
const LARGE_INTEGER* pDueTime, // 초기 시간. 몇 초 뒤에 알람을 울릴지
LONG lPeriod, // 반복 시간. 초기 시간 이후 몇초마다 반복해서 알람을 울릴지. 이 경우 반드시 자동 리셋 타이머로 생성해야 함.
PTIMERAPCROUTINE pfnCompletionRoutine, //
LPVOID lpArgToCompletionRoutine,
BOOL fResume
)
/*
ManualResetTimer.cpp
프로그램 설명: 수동 리셋 타이머 오브젝트에 대한 이해.
*/
#define _WIN32_WINNT 0x0400
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hTimer = NULL;
LARGE_INTEGER liDueTime;
liDueTime.QuadPart=-100000000; // 음수를 주게끔 정의되어 있음. 현재 시간이 0초이고, 상대시간을 설정할땐 음수를 줌.
// ns 단위.
hTimer = CreateWaitableTimer(NULL, FALSE, _T("WaitableTimer"));
if (!hTimer)
{
_tprintf( _T("CreateWaitableTimer failed (%d)\n"), GetLastError());
return 1;
}
_tprintf( _T("Waiting for 10 seconds...\n"));
SetWaitableTimer(hTimer, &liDueTime, 0, NULL, NULL, FALSE);
WaitForSingleObject(hTimer, INFINITE);
_tprintf( _T("Timer was signaled.\n") );
MessageBeep(MB_ICONEXCLAMATION);
return 0;
}
/*
PeriodicTimer.cpp
프로그램 설명: 주기적 타이머에 대한 이해.
*/
#define _WIN32_WINNT 0x0400
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hTimer = NULL;
LARGE_INTEGER liDueTime;
liDueTime.QuadPart=-100000000;
hTimer = CreateWaitableTimer(NULL, FALSE, _T("WaitableTimer"));
if (!hTimer)
{
_tprintf( _T("CreateWaitableTimer failed (%d)\n"), GetLastError());
return 1;
}
// FALSE -> 자동 리셋 타이머
_tprintf( _T("Waiting for 10 seconds...\n"));
SetWaitableTimer(hTimer, &liDueTime, 5000, NULL, NULL, FALSE); // 이 함수의 인자 5000은 ms 단위.
while(1)
{
WaitForSingleObject(hTimer, INFINITE);
_tprintf( _T("Timer was signaled.\n") );
MessageBeep(MB_ICONEXCLAMATION);
}
return 0;
}
임계 영역 접근 동기화의 경우 많은 소프트웨어에서 사용됨.
고급 소프트웨어일수록, 프로그램의 규모가 크고 복잡해질 수록 이벤트, 타이머 기법이 중요해짐.
이는 누군가가 시키는 일만 할 경우 타이머가 필요 없기 때문. 주기적으로 소프트웨어가 스스로 일을 할 능력이 필요할 경우 타이머와 이벤트가 필요하다.