<쓰레드 동기화 기법 - (2)>
[공부했던 것을 되짚어보며]
https://sevenshards.tistory.com/62
이전 글에서 다뤘던 내용은 임계 영역에 대한 개념을 이해하고 이에 대한 동기화 방법에 대한 것이었습니다.
'메모리 접근에 대한 동기화'를 다뤘었고, 이번 글에서는 '쓰레드의 실행순서'를 위주로 다루고자 합니다.
[실행순서에 있어서의 동기화]
"쓰레드의 실행순서를 동기화한다"는 말은 정확하게 표현하면 다음과 같습니다.
"메모리에 접근하는 쓰레드의 실행순서를 동기화한다"
다시 말해서 "실행순서 동기화"에는 "메모리 접근 동기화"라는 개념이 같이 포함이 되어있습니다.
그런데 포커스는 "실행순서"에 맞춰져 있기 때문에 "실행순서 동기화"라고 따로 표현을 한 것입니다.
[생산자/소비자 모델]
"실행순서의 동기화"에 대해서 설명을 할 때 주로 사용되는 모델이 "생산자/소비자 모델"입니다.
이 모델은 입/출력 모델을 기준으로 볼 수도 있고, 다른 한 편으로는 쓰레드를 기준으로 볼 수도 있습니다.
여기서 입력을 받은 데이터를 처리하고 버퍼에 데이터를 입력하는 쓰레드는 '생산자 쓰레드'가 됩니다.
그리고 버퍼에 들어있는 데이터를 가져와 출력하는 쓰레드를 '소비자 쓰레드'라고 합니다.
만약 이걸 단일 쓰레드로 버퍼를 두지 않고수행하게 되면 문제가 생길 수 있습니다.
단적인 예로, 출력 속도가 입력 속도를 따라가지 못하는 경우입니다.
출력은 일정한 속도를 유지하며 출력을 하게 되지만, 입력은 데이터의 양에 의존적입니다.
그래서 입력 데이터의 양이 많아지게 되면 쓰레드 하나로는 처리하지 못하는 상황이 생기게 됩니다.
이와 같은 문제점을 해결하기 위해서 제시한 모델이 위의 그림인 "생산자/소비자 모델"입니다.
중간에 버퍼를 두기 때문에 입력과 출력의 속도가 다르더라도 독립적으로 실행하는 것이 가능합니다.
그리고 실행의 순서는 반드시 "생산자 → 소비자"의 순서로 실행이 되어야 합니다.
[이벤트(Event) 기반 동기화]
위와 같이 실행의 순서가 중요할 때 사용할 수 있는 동기화 기법이 바로 "이벤트(Event) 기반 동기화" 입니다.
여기서도 마찬가지로 "이벤트 오브젝트(이하 이벤트)"라는 오브젝트를 사용하게 됩니다.
그리고 이전 글에서도 임계 영역 접근과 관련하여 사용했던 함수들이 기억이 나실 것입니다.
오브젝트를 생성하고 소멸해줘야 하는 것처럼 이벤트도 생성과 소멸을 담당하는 함수가 있습니다.
마찬가지로 오브젝트를 소유하고 반환하는 함수도 있습니다.
그렇다면 이벤트 기반 동기화에서는 이벤트를 어떻게 사용하는지에 대해서 알아보겠습니다.
우선 생산자 쓰레드가 Non-Signaled 상태의 이벤트를 획득하여 작업을 수행합니다.
소비자 쓰레드는 이벤트를 획득하기 위해 WaitForSingledObject 함수를 통해서 블로킹 상태에 들어가게 됩니다.
생산자 쓰레드가 작업을 마친 뒤에는 이벤트를 Signaled 상태로 바꾸면서 작업의 종료를 알리게 됩니다.
이 때 소비자 쓰레드는 이벤트의 상태가 바뀐 것을 인지하게 되고 블로킹 상태에서 빠져나와 작업을 수행하게 됩니다.
여기서 이벤트를 Signaled 상태로 바꾸는 것은 SetEvent 함수를 통해 수동으로 변경해줘야 합니다.
반대로 Signaled → Non-Signaled 상태로 바꾸는 것은 두 가지의 방법이 있습니다.
1. 수동 리셋 모드
수동 리셋 모드의 경우에는 ResetEvent라는 함수 호출을 통해서 직접적으로 Non-Signaled 상태로 변경해야 합니다.
즉, WaitForSingleObject 함수를 통해서 Non-Signaled 상태가 되지 않습니다.
그래서 둘 이상의 소비자 쓰레드가 대기 중일때 생산자 쓰레드에서 Signaled 상태로 바꾸면 모든 쓰레드가 실행됩니다.
2. 자동 리셋 모드
수동 리셋 모드와 반대로 WaitForSingleObject에 의해서 자동으로 Non-Signaled 상태로 변경하게 됩니다.
그래서 둘 이상의 소비자 쓰레드가 대기 중인 상황일 때 생산자 쓰레드에서 이벤트를 Signaled 상태로 바꾸게 되면 WaitForSingleObject에서 블로킹되어 있던 쓰레드 중 하나만 실행이 됩니다.
이제 예제 코드를 통해서 구현한 생산자/소비자 모델의 예를 보도록 하겠습니다.
[StringEvent.cpp]
/*
* Windows System Programming - 쓰레드 동기화 기법(2)
* 파일명: StringEvent.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-08
* 이전 버전 작성 일자:
* 버전 내용: 이벤트 기반 동기화 기법 - Event 기반 동기화 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
// 생성한 쓰레드에서 사용할 함수 선언
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam);
TCHAR string[100];
HANDLE hEvent;
// main 쓰레드, 생산자의 역할
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hThread;
DWORD dwThreadID;
// Event 생성
hEvent = CreateEvent(
NULL, // 보안(상속) 속성, 상속하지 않으므로 NULL
TRUE, // manual-reset mode로, FALSE로 주면 auto-reset mode
FALSE, // Non-Signaled 상태로 생성
NULL // 이름 설정, 여기서는 안쓰므로 NULl
);
if (hEvent == NULL) // 이벤트 생성 실패 시
{
_tprintf(TEXT("Event object Creation Error\n"));
return -1;
}
// 쓰레드 생성, 소비자의 역할
hThread = (HANDLE)_beginthreadex(
NULL, // 보안(상속) 속성, 상속하지 않으므로 NULL
0, // 쓰레드의 스택 크기, 0으로 줄 경우 디폴트값(1M)
OutputThreadFunction, // 쓰레드에서 실행할 함수
NULL, // 함수에 전달할 인자
0, // 생성과 동시에 실행, 생성하자마자 실행하지 않으려면 CREATE_SUSPENDED 속성
(unsigned*)&dwThreadID // 쓰레드의 ID를 저장할 변수의 주소값 설정
);
if (hThread == NULL) // 쓰레드 생성 실패 시
{
_tprintf(TEXT("Thread object Creation Error\n"));
return -1;
}
// 문자열을 입력받는다. (main 쓰레드 == 생산자)
_fputts(TEXT("Insert string: "), stdout);
_fgetts(string, 30, stdin);
// 이벤트를 Signaled 상태로 변경
SetEvent(hEvent);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hEvent);
CloseHandle(hThread);
return 0;
}
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam)
{
// Event가 Signaled 상태가 될 때까지 쓰레드는 블로킹된 상태가 된다.
WaitForSingleObject(hEvent, INFINITE);
// 그리고 현재 Event는 manual-reset 모드로 생성
// WaitForSingleObject 함수 호출 이후에도 계속 Signaled 상태를 유지하게 됨
// 여기서는 굳이 상태를 변경할 필요가 없으므로 넣지는 않았으나 엄밀히 따지면 넣는 것이 맞음
// Signaled 상태의 Event를 Non-Signaled로 변경
// ResetEvent(hEvent);
// Event가 Signaled 상태가 되면 아래의 문장을 수행
_fputts(TEXT("Output String: "), stdout);
_fputts(string, stdout);
// 임계 영역이 끝이 나면 이벤트를 다시 Signaled 상태로 변경
// SetEvent(hEvent);
return 0;
}
여기서는 main 쓰레드를 포함해서 총 두 개의 쓰레드가 동작을 하고 있습니다.
main 쓰레드는 생산자, 다른 쓰레드는 소비자의 역할을 지니게 됩니다.
[수동 리셋(Manual-Reset) 모드 이벤트(Event)의 활용 예]
앞서 제시한 예제 코드는 소비자가 하나이기 때문에 수동 리셋 모드가 아닌 자동 리셋 모드로 구현을 해도 됐습니다.
그런데 소비자가 둘 이상이라면 수동 리셋 모드를 사용하는 것을 고려해볼 수 있습니다.
예제 코드를 통해서 확인해보겠습니다.
[StringEvent2.cpp]
/*
* Windows System Programming - 쓰레드 동기화 기법(2)
* 파일명: StringEvent2.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-08
* 이전 버전 작성 일자:
* 버전 내용: 이벤트 기반 동기화 기법 - Manual-Reset 모드를 활용한 Event 기반 동기화 예제
* 이전 버전 내용:
*/
#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;
// main 쓰레드, 생산자의 역할
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hThread[2];
DWORD dwThreadID[2];
// Event 생성
hEvent = CreateEvent(
NULL, // 보안(상속) 속성, 상속하지 않으므로 NULL
TRUE, // manual-reset mode로, FALSE로 주면 auto-reset mode
FALSE, // Non-Signaled 상태로 생성
NULL // 이름 설정, 여기서는 안쓰므로 NULl
);
if (hEvent == NULL) // 이벤트 생성 실패 시
{
_tprintf(TEXT("Event object Creation Error\n"));
return -1;
}
// 쓰레드 생성, 소비자1
hThread[0] = (HANDLE)_beginthreadex(
NULL, // 보안(상속) 속성, 상속하지 않으므로 NULL
0, // 쓰레드의 스택 크기, 0으로 줄 경우 디폴트값(1M)
OutputThreadFunction, // 쓰레드에서 실행할 함수
NULL, // 함수에 전달할 인자
0, // 생성과 동시에 실행, 생성하자마자 실행하지 않으려면 CREATE_SUSPENDED 속성
(unsigned*)&dwThreadID[0] // 쓰레드의 ID를 저장할 변수의 주소값 설정
);
// 쓰레드 생성, 소비자2
hThread[1] = (HANDLE)_beginthreadex(
NULL, // 보안(상속) 속성, 상속하지 않으므로 NULL
0, // 쓰레드의 스택 크기, 0으로 줄 경우 디폴트값(1M)
CountThreadFunction, // 쓰레드에서 실행할 함수
NULL, // 함수에 전달할 인자
0, // 생성과 동시에 실행, 생성하자마자 실행하지 않으려면 CREATE_SUSPENDED 속성
(unsigned*)&dwThreadID[1] // 쓰레드의 ID를 저장할 변수의 주소값 설정
);
if (hThread[0] == NULL || hThread[1] == NULL) // 쓰레드 생성 실패 시
{
_tprintf(TEXT("Thread object Creation Error\n"));
return -1;
}
// 문자열을 입력받는다. (main 쓰레드 == 생산자)
_fputts(TEXT("Insert string: "), stdout);
_fgetts(string, 30, stdin);
// 이벤트를 Signaled 상태로 변경
SetEvent(hEvent);
WaitForMultipleObjects(
2,
hThread,
TRUE,
INFINITE
);
CloseHandle(hEvent);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
return 0;
}
unsigned int WINAPI OutputThreadFunction(LPVOID lpParam)
{
// Event가 Signaled 상태가 될 때까지 쓰레드는 블로킹된 상태가 된다.
WaitForSingleObject(hEvent, INFINITE);
// 그리고 현재 Event는 manual-reset 모드로 생성
// WaitForSingleObject 함수 호출 이후에도 계속 Signaled 상태를 유지하게 됨
// 여기서는 굳이 상태를 변경할 필요가 없으므로 넣지는 않았으나 엄밀히 따지면 넣는 것이 맞음
// Signaled 상태의 Event를 Non-Signaled로 변경
// ResetEvent(hEvent);
// Event가 Signaled 상태가 되면 아래의 문장을 수행
_fputts(TEXT("Output String: "), stdout);
_fputts(string, stdout);
// 임계 영역이 끝이 나면 이벤트를 다시 Signaled 상태로 변경
// SetEvent(hEvent);
return 0;
}
unsigned int WINAPI CountThreadFunction(LPVOID lpParam)
{
// Event가 Signaled 상태가 될 때까지 쓰레드는 블로킹된 상태가 된다.
WaitForSingleObject(hEvent, INFINITE);
// 그리고 현재 Event는 manual-reset 모드로 생성
// WaitForSingleObject 함수 호출 이후에도 계속 Signaled 상태를 유지하게 됨
// 여기서는 굳이 상태를 변경할 필요가 없으므로 넣지는 않았으나 엄밀히 따지면 넣는 것이 맞음
// Signaled 상태의 Event를 Non-Signaled로 변경
// ResetEvent(hEvent);
// Event가 Signaled 상태가 되면 아래의 문장을 수행
_tprintf(TEXT("Output String Length: %lld\n"), _tcslen(string)-1);
// 임계 영역이 끝이 나면 이벤트를 다시 Signaled 상태로 변경
// SetEvent(hEvent);
return 0;
}
앞선 예제와 달리 main 쓰레드는 생산자, 두 개의 쓰레드는 소비자의 역할로 구현된 코드입니다.
수동 리셋 모드이기 때문에 WaitForSingleObject 함수에 의해서 자동으로 Non-Signaled 상태로 바뀌지 않습니다.
그래서 두 쓰레드가 모두 실행되는 결과를 볼 수 있습니다.
그리고 코드에서는 ResetEvent 함수를 사용하지 않았기 때문에 자동 모드로 바꿔서 실행을 해보시기 바랍니다.
한 쓰레드만 실행이 된 이후 다른 쓰레드는 블로킹 상태에 빠지는 것을 확인하실 수 있습니다.
[이벤트(Event)+ 뮤텍스(Mutex)]
사실 위의 예제에는 문제점이 있습니다.
아마 여러분이 기대하신 결과대로 Output String과 Output String Length가 순서대로 나왔을 것입니다.
그런데 실제로는 순서대로 나오지 않을 수도 있습니다.
Output String: Output String Length : 10
I love you
위와 같이 뒤죽박죽 섞인 결과가 나올 수도 있습니다.
이는 두 개의 소비자 쓰레드가 동시에 실행이 되면서 생기는 문제입니다.
지금까지 공부했던 것만 놓고 따진다면 각각의 쓰레드의 메인 함수에 의해 실행되는 부분은 임계영역이 아닙니다.
둘 이상의 쓰레드에 의해서 동일한 메모리 공간에 접근하는 상황이 아니기 때문입니다.
그렇다면 생산자/소비자 모델의 측면에서 접근한다고 봤을 때 이 또한 맞지 않습니다.
둘 다 소비자 쓰레드에 의해서 호출이 되는 함수이기 때문입니다.
그래서 앞선 예제를 통해서 아셔야하는 것은 '동기화'라는 개념을 이론적으로만 보면 안된다는 것입니다.
단순하게 임계 영역이나 생산자/소비자 모델의 측면만 볼 것이 아니라 포괄적인 부분을 고려해야 합니다.
이제 위 예제에서 문제가 있는 부분을 알게 되었습니다.
생산자/소비자 측면(main 쓰레드와 두 개의 쓰레드)에서는 이벤트 기반의 동기화 기법을 적용합니다.
그리고 두 소비자 쓰레드에 대해서는 뮤텍스 기반의 동기화를 사용하여 해결해보고자 합니다.
[이벤트와 뮤텍스 오브젝트 적용 예제]
[StringEvent3.cpp]
/*
* Windows System Programming - 쓰레드 동기화 기법(3)
* 파일명: StringEvent3.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-08
* 이전 버전 작성 일자:
* 버전 내용: 이벤트 기반 동기화 기법 - Event + Mutex 기반 동기화 예제
* 이전 버전 내용:
*/
#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;
// main 쓰레드, 생산자의 역할
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hThread[2];
DWORD dwThreadID[2];
// Event 생성, 순서를 동기화하기 위해 사용
gSynString.hEvent = CreateEvent(
NULL, // 보안(상속) 속성, 상속하지 않으므로 NULL
TRUE, // manual-reset mode로, FALSE로 주면 auto-reset mode
FALSE, // Non-Signaled 상태로 생성
NULL // 이름 설정, 여기서는 안쓰므로 NULL
);
if (gSynString.hEvent == NULL) // 이벤트 생성 실패 시
{
_tprintf(TEXT("Event object Creation Error\n"));
return -1;
}
// Mutex 생성, 소비자 쓰레드가 둘이므로 쓰레드의 동기화에 사용
gSynString.hMutex = CreateMutex(
NULL, // 보안(상속) 속성, 상속하지 않으므로 NULL
FALSE, // 누구나 소유할 수 있음 -> 선점형
NULL // 이름 설정, 여기서는 안쓰므로 NULL
);
if (gSynString.hMutex == NULL) // 뮤텍스 생성 실패 시
{
_tprintf(TEXT("Event object Creation Error\n"));
return -1;
}
// 쓰레드 생성, 소비자1
hThread[0] = (HANDLE)_beginthreadex(
NULL, // 보안(상속) 속성, 상속하지 않으므로 NULL
0, // 쓰레드의 스택 크기, 0으로 줄 경우 디폴트값(1M)
OutputThreadFunction, // 쓰레드에서 실행할 함수
NULL, // 함수에 전달할 인자
0, // 생성과 동시에 실행, 생성하자마자 실행하지 않으려면 CREATE_SUSPENDED 속성
(unsigned*)&dwThreadID[0] // 쓰레드의 ID를 저장할 변수의 주소값 설정
);
// 쓰레드 생성, 소비자2
hThread[1] = (HANDLE)_beginthreadex(
NULL, // 보안(상속) 속성, 상속하지 않으므로 NULL
0, // 쓰레드의 스택 크기, 0으로 줄 경우 디폴트값(1M)
CountThreadFunction, // 쓰레드에서 실행할 함수
NULL, // 함수에 전달할 인자
0, // 생성과 동시에 실행, 생성하자마자 실행하지 않으려면 CREATE_SUSPENDED 속성
(unsigned*)&dwThreadID[1] // 쓰레드의 ID를 저장할 변수의 주소값 설정
);
if (hThread[0] == NULL || hThread[1] == NULL) // 쓰레드 생성 실패 시
{
_tprintf(TEXT("Thread object Creation Error\n"));
return -1;
}
// 문자열을 입력받는다. (main 쓰레드 == 생산자)
_fputts(TEXT("Insert string: "), stdout);
_fgetts(gSynString.string, 30, stdin);
// 이벤트를 Signaled 상태로 변경
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)
{
// Event가 Signaled 상태가 될 때까지 쓰레드는 블로킹된 상태가 된다.
WaitForSingleObject(gSynString.hEvent, INFINITE);
WaitForSingleObject(gSynString.hMutex, INFINITE);
// 그리고 현재 Event는 manual-reset 모드로 생성
// WaitForSingleObject 함수 호출 이후에도 계속 Signaled 상태를 유지하게 됨
// 여기서는 굳이 상태를 변경할 필요가 없으므로 넣지는 않았으나 엄밀히 따지면 넣는 것이 맞음
// Signaled 상태의 Event를 Non-Signaled로 변경
// ResetEvent(hEvent);
// Event가 Signaled 상태가 되면 아래의 문장을 수행
_fputts(TEXT("Output String: "), stdout);
_fputts(gSynString.string, stdout);
// 임계 영역이 끝이 나면 이벤트를 다시 Signaled 상태로 변경
// SetEvent(hEvent);
ReleaseMutex(gSynString.hMutex);
return 0;
}
unsigned int WINAPI CountThreadFunction(LPVOID lpParam)
{
// Event가 Signaled 상태가 될 때까지 쓰레드는 블로킹된 상태가 된다.
WaitForSingleObject(gSynString.hEvent, INFINITE);
WaitForSingleObject(gSynString.hMutex, INFINITE);
// 그리고 현재 Event는 manual-reset 모드로 생성
// WaitForSingleObject 함수 호출 이후에도 계속 Signaled 상태를 유지하게 됨
// 여기서는 굳이 상태를 변경할 필요가 없으므로 넣지는 않았으나 엄밀히 따지면 넣는 것이 맞음
// Signaled 상태의 Event를 Non-Signaled로 변경
// ResetEvent(hEvent);
// Event가 Signaled 상태가 되면 아래의 문장을 수행
_tprintf(TEXT("Output String Length: %lld\n"), _tcslen(gSynString.string) - 1);
// 임계 영역이 끝이 나면 이벤트를 다시 Signaled 상태로 변경
// SetEvent(hEvent);
ReleaseMutex(gSynString.hMutex);
return 0;
}
이제 확실한 결과를 볼 수 있는 코드가 되었습니다.
[타이머(Timer) 기반 동기화]
이번에 다루게 될 동기화 기법은 타이머(Timer) 기반의 동기화 기법입니다.
타이머라는 이름 그대로 정해진 시간이 지나면 자동으로 Signaled 상태가 되는 특성을 지닌 동기화 오브젝트입니다.
타이머의 정확한 명칭은 "Waitable Timer"지만, 간략하게 줄여서 타이머라고 하겠습니다.
타이머를 기반으로 쓰레드를 동기화하는 것은 임계 영역의 문제 해결을 위한 동기화와는 조금 관점이 다릅니다.
타이머를 이용한 동기화는 쓰레드의 실행시간 및 실행주기를 결정하겠다는 의미를 지닙니다.
[수동 리셋 타이머(Manual-Reset Timer)]
가장 기본이 되는 수동 리셋 타이머에 대해서 먼저 알아보겠습니다.
알람 시계로 친다면 정해진 시간이 지나면 딱 한 번 울리는 알람 시계를 생각하면 됩니다.
HANDLE CreateWaitableTimer(
LPSECURITY_ATTRIBUTES lpTimerAttributes,
BOOL bManualReset, // 자동 리셋(FALSE), 수동 리셋 설정(TRUE) - 이벤트와 동일
LPCWSTR lpTimerName
);
BOOL SetWaitableTimer(
HANDLE hTimer,
const LARGE_INTEGER *lpDueTime, // 원하는 시간 설정
LONG lPeriod, // 주기적인 시간 설정
PTIMERAPCROUTINE pfnCompletionRoutine,
LPVOID lpArgToCompletionRoutine,
BOOL fResume
);
위의 두 함수는 타이머를 생성하고 설정하는데 사용하는 함수입니다.
이벤트와 비교했을 때, 타이머는 인자가 하나 적습니다.
바로 생성시 Non-Signaled 상태로 둘 것인지, Signaled 상태로 둘 것인지를 정하는 인자가 없습니다.
그 이유는 일정 시간이 지나면 Signaled 상태가 되는 오브젝트이기 때문입니다.
그래서 타이머는 생성을 하면 반드시 Non-Signaled 상태로 생성이 된다는 점을 기억해두시길 바랍니다.
SetWaitableTimer의 세 번째 인자를 0으로 주게 되면 수동 리셋 타이머가 됩니다.
그리고 0이 아닌 값을 주게 되면 주기적 타이머(Periodic-Timer)가 되는데 이는 따로 예제를 통해 확인하겠습니다.
우선 여기서는 수동 리셋 타이머의 예제 코드를 보도록 하겠습니다.
[Timer.cpp]
/*
* Windows System Programming - 쓰레드 동기화 기법(2)
* 파일명: Timer.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-08
* 이전 버전 작성 일자:
* 버전 내용: 타이머 기반 동기화 기법 - Timer 기반 동기화 예제
* 이전 버전 내용:
*/
// SetWaitableTimer 함수를 호출하기 위한 매크로
#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, // 보안(상속) 속성
FALSE, // auto-reset 모드로 설정, TRUE로 하면 manual-reset
NULL // 이름을 붙여줄 경우에 사용하는 인자
);
if (!hTimer)
{
_tprintf(TEXT("CreateWaitableTimer failed (%d)\n"), GetLastError());
return -1;
}
_tprintf(TEXT("Waiting for 10 seconds...\n"));
SetWaitableTimer(
hTimer, // 알람을 설정할 타이머의 핸들
&liDueTime, // 알람이 울릴 시간
0, // 타이머를 주기적으로 울리기 위한 인자, 0이면 주기적으로 동작 안함
NULL, // 완료 루틴 타이머, 이후에 다룸
NULL, // 완료 루틴 타이머, 이후에 다룸
FALSE // Resume 인자, 전원 관리와 관련 있는 변수. 기본적으로는 FALSE
);
WaitForSingleObject(hTimer, INFINITE);
_tprintf(TEXT("Timer was signaled\n"));
MessageBeep(MB_ICONEXCLAMATION);
return 0;
}
해당 코드를 실행하면 10초 뒤에 main 쓰레드가 MessageBeep라는 함수를 동작하게 됩니다.
[주기적 타이머(Periodic-Timer)]
마지막으로 주기적 타이머에 대해서 다뤄보도록 하겠습니다.
주기적 타이머는 자동 리셋 타이머라고 부르기도 합니다.
알람 시계로 비유하자면 알람이 울린 이후에도 5분마다 주기적으로 알람을 울리게 하는 것과 같습니다.
다시 말해서 '수동 리셋 타이머 + α'의 개념으로 보시면 될 것 같습니다.
[PeriodicTimer.cpp]
/*
* Windows System Programming - 쓰레드 동기화 기법(2)
* 파일명: PeriodicTimer.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-08
* 이전 버전 작성 일자:
* 버전 내용: 타이머 기반 동기화 기법 - Periodic Timer (주기적 타이머) 기반 동기화 예제
* 이전 버전 내용:
*/
// SetWaitableTimer 함수를 호출하기 위한 매크로
#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, // 보안(상속) 속성
FALSE, // auto-reset 모드로 설정, TRUE로 하면 manual-reset
NULL // 이름을 붙여줄 경우에 사용하는 인자
);
if (!hTimer)
{
_tprintf(TEXT("CreateWaitableTimer failed (%d)\n"), GetLastError());
return -1;
}
_tprintf(TEXT("Waiting for 10 seconds...\n"));
SetWaitableTimer(
hTimer, // 알람을 설정할 타이머의 핸들
&liDueTime, // 알람이 울릴 시간
5000, // 타이머를 주기적으로 울리기 위한 인자, 5초 간격으로 동작
NULL, // 완료 루틴 타이머, 이후에 다룸
NULL, // 완료 루틴 타이머, 이후에 다룸
FALSE // Resume 인자, 전원 관리와 관련 있는 변수. 기본적으로는 FALSE
);
while (1)
{
WaitForSingleObject(hTimer, INFINITE);
_tprintf(TEXT("Timer was signaled\n"));
MessageBeep(MB_ICONEXCLAMATION);
}
return 0;
}
해당 예제는 10초 뒤에 알람이 울린 이후, 5초 간격으로 알람이 계속 울리는 코드입니다.
여기서 타이머를 생성할 때 두 번째 인자에 FALSE를 줬습니다.
이는 이벤트 때와 마찬가지로 WaitForSingleObject 함수에 의해 자동으로 Non-Signaled 상태가 됩니다.
이와 같이 준 이유는 주기적으로 타이머가 작동해야 하므로 다시 Non-Signaled 상태로 돌아갈 필요가 있기 때문입니다.
그래서 두 번째 인자에 TRUE를 주면 주기적 타이머는 10초 뒤에 울리는 알람 이후에는 더 이상 동작하지 않습니다.