2025. 5. 13. 00:35ㆍ개발/Windows
이 글은 윤성우 님의 '뇌를 자극하는 윈도우즈 시스템 프로그래밍'을 참고하여 공부한 내용입니다.
저는 일반적으로 동기화라면 어떤 행동을 동시에 진행하거나 다른 두 개 이상의 매체의 상태을 동일하게 유지하는 것으로 알고 있었습니다. 그런데 공부를 해보니 스레드에서 동기화는 다른 의미를 지니고 있었습니다. 오히려 반대의 개념에 더 가까운 것 같습니다. 스레드에서 동기화는 스레드들이 동시에 접근할 수 있는 공유 자원을 순차적으로 접근할 수 있게 해주는 객체입니다. 결국 스레드를 사용하는 목적이 어떻든 동기화의 본질은 순서인 것 같습니다.
Synchronization in user mode
유저 모드 동기화는 말 그래로 유저 모드에서 동기화가 이뤄지며 커널 모드에 진입하지 않기 때문에 성능에 큰 이점이 있습니다. 하지만 커널 오브젝트를 사용하지 않기 때문에 다른 프로세스와의 동기화는 지원되지 않습니다. 그러므로 동일한 프로세스 내부 스레드의 동기화는 유저 모드 동기화를 사용하는 것이 좋습니다.
Critical Section
여기서 말하는 Critical Section은 동기화 간 공유자원에 접근하는 임계 영역을 뜻하는 것이 아닌 유저 모드 동기화 객체를 뜻합니다. Critical Section은 단일 스레드만 임계 영역에 진입할 수 있습니다. 이때 유저모드에서 임계 영역에 진입할 수 있는지를 판별하기 때문에 방식이 스핀락과 비슷합니다. 하지만 Critical Section은 스핀락과 같이 계속해서 판별을 하는 것이 아닌 진입할 수 없다고 판단되면 대기상태가 됩니다. 즉 Block상태가 됩니다. 그래서 CPU 자원효율이 스핀락 보다 우수합니다. 하지만 그렇기에 Critical Section은 콘텍스트 스위치에 의한 성능저하가 발생합니다.
Critical Section은 일반적으로 아래의 함수들을 통해 사용할 수 있습니다.
void InitializeCriticalSection(
[out] LPCRITICAL_SECTION lpCriticalSection
);
이 함수를 통해 Critical Section 객체를 사용하기 전 초기화를 할 수 있습니다. 매개 변수는 반환받을 Critical Section 객체의 포인터 하나입니다. 유저 모드이기 때문에 핸들을 반환하지 않습니다.
void EnterCriticalSection(
[in, out] LPCRITICAL_SECTION lpCriticalSection
);
이 함수를 통해 임계 영역에 진입할 수 있으며 만약 다른 스레드가 임계 영역에 있다면 현재 스레드는 대기상태가 됩니다. 매개 변수는 사용할 Critical Section 객체의 포인터입니다.
void LeaveCriticalSection(
[in, out] LPCRITICAL_SECTION lpCriticalSection
);
이 함수는 임계 영역에 진입한 스레드가 임계 영역을 퇴장할 때 사용할 수 있습니다. 만약 임계영역에 진입하지 않은 스레드가 이 함수를 호출하는 경우 위 EnterCriticalSection 함수를 호출하는 다른 스레드가 무한대기 될 수 있는 오류가 발생합니다. 매개 변수는 사용할 Critical Section 객체의 포인터입니다.
void DeleteCriticalSection(
[in, out] LPCRITICAL_SECTION lpCriticalSection
);
이 함수는 초기화된 Critical Section 객체를 반환할 수 있습니다. 만약 초기화 되지 않은 Critical Section 객체를 사용할 경우 메모리 손상이 발생된 예기치 못한 오류가 발생할 수 있습니다. 매개 변수는 해제할 Critical Section 객체의 포인터입니다.
위 함수들을 사용하여 예시를 작성해 보겠습니다.
#include <windows.h>
#include <iostream>
#include <thread>
#define NUM_THREAD 3
CRITICAL_SECTION cs;
unsigned int WINAPI ThreadFunc(LPVOID lpParam) {
DWORD* id = (DWORD*)lpParam;
EnterCriticalSection(&cs);
std::cout << "Thread " << *id << " in critical section\n";
Sleep(1000);
std::cout << "Thread " << *id << " leaving critical section\n";
LeaveCriticalSection(&cs);
return 0;
}
int main() {
DWORD dwThreadId[NUM_THREAD];
DWORD dwThreadName[NUM_THREAD];
HANDLE hThread[NUM_THREAD];
InitializeCriticalSection(&cs);
for (DWORD i = 0; i < NUM_THREAD; ++i)
{
dwThreadName[i] = i + 1;
hThread[i] = (HANDLE)_beginthreadex
(NULL, 0, ThreadFunc, &dwThreadName[i], CREATE_SUSPENDED, (unsigned*)&dwThreadId[i]);
}
for (DWORD i = 0; i < NUM_THREAD; ++i)
ResumeThread(hThread[i]);
WaitForMultipleObjects(NUM_THREAD, hThread, TRUE, INFINITE);
for (DWORD i = 0; i < NUM_THREAD; ++i)
CloseHandle(hThread[i]);
DeleteCriticalSection(&cs);
return 0;
}
Thread 2 in critical section
Thread 2 leaving critical section
Thread 3 in critical section
Thread 3 leaving critical section
Thread 1 in critical section
Thread 1 leaving critical section
Interlocked Function
유저 모드 동기화 방법 중 하나이며 특징은 CPU의 단일 명령어 처리 기능을 통해 단일 변수에 대해 원자적 연산을 처리합니다. CPU의 lock 명령어를 통해 다른 스레드가 중간에 해당 변수에 접근할 경우 메모리에 잠금을 걸어 끼어들지 못하게 하는 것입니다. 그래서 스레드가 대기하지 않지만 동기적으로 연산이 가능하게 됩니다. 주로 단일 정수/포인터 변수의 연산에 사용됩니다. 또한 대기를 하지 않기 때문에 성능의 이점이 유저 모드 동기화 방법 중 가장 좋습니다.
Function | Note |
InterlockedIncrement | 32비트 변수의 값을 1 증가 |
InterlockedDecrement | 32비트 변수의 값을 1 감소 |
InterlockedExchange | 32비트 변수의 값을 지정된 값으로 설정 |
InterlockedExchangeAdd | 32비트 변수의 값에 지정된 값을 더함 |
InterlockedCompareExchange | 32비트 변수의 값과 비교할 값이 같을 경우 지정된 값으로 설정 |
InterlockedAnd/Or/Xor | 32비트 변수의 값에 대한 논리 연산 |
InterlockedIncrement64 | 64비트 변수에 대한 연산 |
위에는 Interlocked Function 중 일부입니다. 여기서 주의해야 할 점은 매개변수의 변수가 volatile로 선언되어 있다는 것입니다.
volatile은 컴파일러에 대한 최적화를 방지하는 키워드입니다. volatile을 사용하지 않으면 컴파일러의 최적화에 의해 코드가 축약되어 예상치 못한 오류를 발생시킬 수 있습니다.
아래는 Interlocked Function을 사용한 간단한 동기화 예시입니다.
#include <windows.h>
#include <iostream>
#include <thread>
#define NUM_THREAD 3
#define MAX_COUNT 10000
LONG volatile counter = 0;
unsigned int WINAPI ThreadFunc(LPVOID lpParam) {
for (DWORD i = 0; i < MAX_COUNT; ++i) {
InterlockedIncrement(&counter);
}
return 0;
}
int main() {
DWORD dwThreadId[NUM_THREAD];
HANDLE hThread[NUM_THREAD];
for (DWORD i = 0; i < NUM_THREAD; ++i)
{
hThread[i] = (HANDLE)_beginthreadex
(NULL, 0, ThreadFunc, NULL, CREATE_SUSPENDED, (unsigned*)&dwThreadId[i]);
}
for (DWORD i = 0; i < NUM_THREAD; ++i)
ResumeThread(hThread[i]);
WaitForMultipleObjects(NUM_THREAD, hThread, TRUE, INFINITE);
for (DWORD i = 0; i < NUM_THREAD; ++i)
CloseHandle(hThread[i]);
std::cout << "Final Counter: " << counter << "\n";
return 0;
}
Final Counter: 30000
Synchroniztion in kernel mode
커널 모드 동기화는 커널 모드에 진입하기 때문에 유저 모드 동기화 보다 성능이 느릴 수 있습니다. 하지만 커널 오브젝트를 사용하기 때문에 프로세스 간 동기화가 가능합니다. 또한 유저모드에서 지원하지 않는 동기화 기법을 사용할 수 있습니다. 예를 들어 세마포어를 사용하여 임계 구역에 단일 스레드만 접근을 허용하는 것이 아닌 동시에 특정 수만큼 스레드가 접근할 수 있게 할 수 있습니다.
Mutex
하나의 스레드만 임계 영역에 접근할 수 있도록 보장하는 커널모드 동기화 객체입니다. Mutex는 상호 배제, Mutual Exclusion의 약자로 데이터의 무결성을 유지하기 위해 범용적으로 사용됩니다. 커널모드 동기화이기 때문에 이름을 붙여 다른 프로세스에서도 사용이 가능합니다. 하지만 커널 모드 진입과 상호 배제에 의해 처리속도가 느리기 때문에 성능보다 안전성을 높이기 위해 사용됩니다.
Mutex를 획득하면 임계영역에 접근할 수 있으며 Mutex는 Lock 상태가 됩니다. 만약 이미 Lock 상태의 Mutex를 획득하려 하면 해당 Mutex가 Unlock 되기 전까지 스레드는 대기상태가 됩니다. 또한 Mutex를 획득한 스레드만 해당 Mutex를 Unlock을 할 수 있습니다.
Mutex를 사용하기 위해서는 아래 함수들을 사용합니다.
HANDLE CreateMutex(
[in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
[in] BOOL bInitialOwner,
[in, optional] LPCWSTR lpName
);
이 함수는 이름이 있는 혹은 익명 Mutex를 생성할 수 있습니다. 반환값은 생성된 Mutex의 HANDLE을 반환합니다. 그리고 매개변수들은 아래와 같습니다.
- lpMutexAttributes: 보안 객체로 핸들의 상속 유무를 결정합니다.
- bInitialOwner: Mutex를 생성과 동시에 호출한 스레드가 권한을 획득할지 유무를 결정합니다.
- lpName: Mutex의 이름입니다. 이름은 대소문자를 구별하며 이 이름을 통해 프로세스 간 동기화를 지원합니다. 익명으로 설정할 경우 NULL을 설정합니다.
BOOL ReleaseMutex(
[in] HANDLE hMutex
);
위 함수는 WaitForSingleObject를 통해 임계 영역에 진입하여 퇴장 시 Mutex를 해제하기 위해 사용합니다. 매개변수는 획득한 Mutex의 HANDLE입니다. 이 함수를 통해 Mutex는 Signaled 상태가 됩니다.
익명 Mutex를 사용하여 간단한 동기화 코드를 작성해 보겠습니다.
#include <windows.h>
#include <iostream>
#include <thread>
#define NUM_THREAD 5
HANDLE hMutex;
unsigned int WINAPI ThreadFunc(LPVOID lpParam) {
DWORD* id = (DWORD*)lpParam;
WaitForSingleObject(hMutex, INFINITE);
std::cout << "Thread " << *id << " in critical section\n";
Sleep(1000);
std::cout << "Thread " << *id << " leaving critical section\n";
ReleaseMutex(hMutex);
return 0;
}
int main() {
DWORD dwThreadId[NUM_THREAD];
DWORD dwThreadName[NUM_THREAD];
HANDLE hThread[NUM_THREAD];
hMutex = CreateMutex(nullptr, FALSE, nullptr);
if (!hMutex)
return 1;
for (DWORD i = 0; i < NUM_THREAD; ++i)
{
dwThreadName[i] = i + 1;
hThread[i] = (HANDLE)_beginthreadex
(NULL, 0, ThreadFunc, &dwThreadName[i], CREATE_SUSPENDED, (unsigned*)&dwThreadId[i]);
}
for (DWORD i = 0; i < NUM_THREAD; ++i)
ResumeThread(hThread[i]);
WaitForMultipleObjects(NUM_THREAD, hThread, TRUE, INFINITE);
for (DWORD i = 0; i < NUM_THREAD; ++i)
CloseHandle(hThread[i]);
CloseHandle(hMutex);
return 0;
}
Thread 4 in critical section
Thread 4 leaving critical section
Thread 1 in critical section
Thread 1 leaving critical section
Thread 3 in critical section
Thread 3 leaving critical section
Thread 5 in critical section
Thread 5 leaving critical section
Thread 2 in critical section
Thread 2 leaving critical section
Mutex를 WaitForSingleObject를 통해 사용할 때 주의해야 할 점이 있습니다. 정상적으로 Mutex가 획득된 경우 WaitForSingleObject의 반환값은 WAIT_OBJECT_0입니다. 만약 다른 스레드가 Mutex를 점유한 상태로 종료된다면 커널에서 해당 스레드가 점유한 Mutex를 해제합니다. 이때 Mutex를 대기하고 있던 스레드의 WaitForSingleObject의 반환값은 WAIT_ABANDONED입니다. 이 반환값을 통해 다른 스레드가 비정상적으로 종료되었는지를 확인할 수 있는 것입니다.
마지막으로 이름 있는 Mutex의 경우 사용방법이 조금 다릅니다. 우선 다른 프로세스가 CreateMutex 호출하여 Mutex를 생성합니다. 그다음 아래 함수를 통해 Mutex를 불러옵니다. 불러온 이름 있는 Mutex는 프로세스 간 동기화를 지원합니다.
HANDLE OpenMutex(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] LPCWSTR lpName
);
이 함수는 CreateMutex 대신 사용하며 이름 있는 Mutex를 불러오는 역할을 합니다. 반환값은 불러온 Mutex의 HANDLE입니다. 그리고 매개변수들은 아래와 같습니다.
- dwDesiredAccess: 불러올 Mutex의 액세스 요청 권한입니다.
- bInheritHandle: 불러올 Mutex의 상속 유무입니다.
- lpName: 불러올 Mutex의 이름입니다.
Semaphore
Mutex와 다르게 상호 배제가 아닌 정수형 카운터 기반의 동기화 객체입니다. Semaphore는 Counter 값을 기준으로 임계 영역의 진입을 대기시킵니다. 만약 Counter의 최댓값이 10이라면 스레드를 최대 10개까지 동시에 임계 영역에 접근할 수 있게 제어합니다. 이후 스레드가 임계영역에 퇴장하면 대기하던 스레드가 진입하여 최댓값만큼 스레드들을 임계영역에 접근시킵니다.
만약 최댓값을 1로 하게 된다면 Mutex와 동일한 상호 배제 방식의 동기화가 됩니다. 상호 배제 Semaphore를 Binary Semaphore라 부릅니다. 그리고 반환될 Counter 값은 고정적이지 않으며 매개변수에 의해 0 이상의 정수 값이 될 수 있습니다. 하지만 Counter가 최댓값을 넘어간다면 오류를 반환합니다.
Semaphore를 제어하기 위한 함수는 아래와 같습니다.
HANDLE CreateSemaphore(
[in, optional] LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
[in] LONG lInitialCount,
[in] LONG lMaximumCount,
[in, optional] LPCSTR lpName
);
이 함수는 이름 있는 혹은 익명 Semaphore를 생성합니다. 반환값은 생선 된 Semaphore의 HANDLE을 반환합니다. 매개변수는 아래와 같습니다.
- lpSemaphoreAttributes: 보안 객체입니다.
- lInitialCount: 초기 Counter 값입니다. 이 값은 lMaximumCount 보다 작아야 합니다.
- lMaximumCount: 최대 Counter 값입니다. 이 값이 0보다 커야 하며 1이면 Binary Semaphore가 됩니다.
- lpName: 생성할 Semaphore의 이름입니다. 익명으로 설정할 경우 NULL을 설정합니다.
중요한 부분은 lInitialCount와 lMaximumCount입니다. Counter 값은 0에서 증가하는 것이 아닌 초기 값에서 감소하는 방식으로 작동합니다. 즉 Counter가 0보다 작아지면 대기가 되는 것입니다. 이 초기값을 결정하는 것이 lInitialCount입니다. 그리고 최댓값은 자원의 전체 개수를 뜻합니다. Semaphore는 WaitForSingleObject 함수를 통해 Counter 값을 1씩 감소시킵니다. 이때 Semaphore를 반환될 때 증가할 Counter 값을 지정할 수 있습니다. 이 값이 최댓값과 비교되어 에러 값을 반환합니다.
BOOL ReleaseSemaphore(
[in] HANDLE hSemaphore,
[in] LONG lReleaseCount,
[out, optional] LPLONG lpPreviousCount
);
이 함수는 감소된 Semaphore의 Counter 값을 증가시키는 역할을 합니다. 값을 증가시키는 것으로 대기 중인 스래드를 임계 영역에 진입시킬 수 있는 것입니다. 반환값은 함수의 성공/실패 여부를 반환합니다. 만약 FALSE로 반환된 경우 GetLastError를 통해 오류를 확인할 수 있습니다. 매개변수는 아래와 같습니다.
- hSemaphore: Counter를 증가시킬 Semaphore HANDLE입니다.
- lReleaseCount: 증가시킬 Counter 값입니다.
- lpPreviousCount: 증가시키기 전 Counter 값을 받을 포인터입니다. NULL을 넣으면 받지 않습니다.
위 함수를 통해 1을 증가시키면 대기 중인 스래드 1개가 임계 영역으로 진입합니다. 또한 이 함수는 임계 영역에 있는 Semaphore 뿐만 아니라 외부에 있는 다른 스래드가 실행할 수 있습니다. 이를 이용해 공유 자원에 접근하는 스래드 수를 자원에 맞추어 조절할 수 있는 것입니다.
아래는 익명 Semaphore를 사용한 간단한 예시입니다.
#include <windows.h>
#include <iostream>
#include <thread>
#define NUM_THREAD 10
HANDLE hSemaphore;
unsigned int WINAPI ThreadFunc(LPVOID lpParam) {
DWORD* id = (DWORD*)lpParam;
WaitForSingleObject(hSemaphore, INFINITE);
std::cout << "Thread " << *id << " in critical section\n";
Sleep(1000);
std::cout << "Thread " << *id << " leaving critical section\n";
ReleaseSemaphore(hSemaphore, 1, NULL);
return 0;
}
int main() {
DWORD dwThreadId[NUM_THREAD];
DWORD dwThreadName[NUM_THREAD];
HANDLE hThread[NUM_THREAD];
hSemaphore = CreateSemaphore(nullptr, 3, 3, nullptr);
if (!hSemaphore)
return 1;
for (DWORD i = 0; i < NUM_THREAD; ++i)
{
dwThreadName[i] = i + 1;
hThread[i] = (HANDLE)_beginthreadex
(NULL, 0, ThreadFunc, &dwThreadName[i], CREATE_SUSPENDED, (unsigned*)&dwThreadId[i]);
}
for (DWORD i = 0; i < NUM_THREAD; ++i)
ResumeThread(hThread[i]);
WaitForMultipleObjects(NUM_THREAD, hThread, TRUE, INFINITE);
for (DWORD i = 0; i < NUM_THREAD; ++i)
CloseHandle(hThread[i]);
CloseHandle(hSemaphore);
return 0;
}
Thread 1 in critical section
Thread 3 in critical section
Thread 2 in critical section
Thread 1 leaving critical section
Thread 4 in critical section
Thread 3 leaving critical section
Thread 5 in critical section
Thread 2 leaving critical section
Thread 6 in critical section
Thread Thread 5 leaving critical section
6 leaving critical section
Thread 7 in critical section
Thread 4 leaving critical section
Thread 8 in critical section
Thread 9 in critical section
Thread 7 leaving critical section
Thread 10 in critical section
Thread 8Thread 9 leaving critical section
leaving critical section
Thread 10 leaving critical section
최대 접근 가능한 스래드 수를 3개로 설정하여 출력이 중첩된 것을 확인할 수 있습니다. 그래서 Semaphore는 한정된 자원에 접근하는 것을 제어하는 용도로 사용하며 주로 DB Connection Pool에 사용됩니다.
Mutex와 동일하게 이름 있는 Semaphore는 다른 프로세스에서 생성한 이름 있는 Semaphore를 아래 함수를 통해 불러와 사용하게 됩니다. 불러온 이름 있는 Semaphore는 프로세스 간 동기화를 지원합니다.
HANDLE OpenSemaphore(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] LPCWSTR lpName
);
이 함수는 다른 프로세스에서 생성된 이름 있는 Semaphore를 불러옵니다. 반환값은 불러온 Semaphore의 HANDLE입니다. 매개변수는 Mutex와 동일합니다.
- dwDesiredAccess: 불러올 Semaphore의 액세스 요청 권한입니다.
- bInheritHandle: 불러올 Semaphore의 상속 유무입니다.
- lpName: 불러올 Semaphore의 이름입니다.
Event
이 객체는 커널 오브젝트의 Signaled와 Non-Signaled 상태를 알림으로 사용하여 특정 작업의 완료 혹은 조건의 만족 등을 통지합니다. 그래서 스레드 간 작업 시점을 동기화할 수 있습니다. Event는 수동 재설정, Manual-Reset 혹은 자동 재설정, Auto-Reset 두 가지 방식을 통해 다중 스래드 실행 순서를 동기화합니다.
Manual-Reset은 커널 오브젝트의 Signaled와 Non-Signaled 상태를 SetEvent와 ResetEvent 함수를 통해 수동으로 전환합니다. Auto-Reset은 커널 오브젝트 상태를 SetEvent와 WaitForSingleObject 함수를 통해 자동으로 전환합니다. 차이점은 WaitForSingleObject를 두 방식이 공통으로 사용하지만 값이 반환된 후 Auto-Reset은 Event 객체를 다시 Non-Signaled 상태로 변경한다는 것입니다.
Event 객체는 아래 함수들을 사용하여 제어할 수 있습니다.
HANDLE CreateEvent(
[in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes,
[in] BOOL bManualReset,
[in] BOOL bInitialState,
[in, optional] LPCSTR lpName
);
이 함수는 이름 있는 혹은 익명 Event 객체를 생성합니다. 반환값은 생성된 Event 객체의 HANDLE이 반환됩니다. 매개변수들은 아래와 같습니다.
- lpEventAttributes: 보안 객체입니다.
- bManualReset: 생성할 Event 객체의 모드를 설정합니다. TRUE면 Manual-Reset FALSE면 Auto-Reset 방식을 사용합니다.
- bInitialState: 생성할 Event 객체의 초기상태입니다. TRUE면 Signaled FALSE면 Non-Signaled입니다.
- lpName: 생성할 Event 객체의 이름입니다. 익명으로 생성할 경우 NULL을 입력합니다.
BOOL SetEvent(
[in] HANDLE hEvent
);
이 함수는 Event 객체를 Signaled 상태로 설정합니다. 매개변수는 설정할 Event 객체의 HANDLE입니다.
BOOL ResetEvent(
[in] HANDLE hEvent
);
이 함수는 Event 객체를 Non-Signaled 상태로 설정합니다. 매개변수는 설정할 Event 객체의 HANDLE입니다.
아래는 Auto-Reset Event를 사용한 간단한 동기화 예시입니다.
#include <windows.h>
#include <iostream>
#include <thread>
#define NUM_THREAD 10
HANDLE hEvent;
unsigned int WINAPI ThreadFunc(LPVOID lpParam) {
DWORD* id = (DWORD*)lpParam;
WaitForSingleObject(hEvent, INFINITE);
std::cout << "Thread " << *id << " in critical section\n";
Sleep(1000);
std::cout << "Thread " << *id << " leaving critical section\n";
SetEvent(hEvent);
return 0;
}
int main() {
DWORD dwThreadId[NUM_THREAD];
DWORD dwThreadName[NUM_THREAD];
HANDLE hThread[NUM_THREAD];
hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (!hEvent)
return 1;
for (DWORD i = 0; i < NUM_THREAD; ++i)
{
dwThreadName[i] = i + 1;
hThread[i] = (HANDLE)_beginthreadex
(NULL, 0, ThreadFunc, &dwThreadName[i], NULL, (unsigned*)&dwThreadId[i]);
}
SetEvent(hEvent);
WaitForMultipleObjects(NUM_THREAD, hThread, TRUE, INFINITE);
for (DWORD i = 0; i < NUM_THREAD; ++i)
CloseHandle(hThread[i]);
CloseHandle(hEvent);
return 0;
}
Thread 1 in critical section
Thread 1 leaving critical section
Thread 2 in critical section
Thread 2 leaving critical section
Thread 3 in critical section
Thread 3 leaving critical section
Thread 4 in critical section
Thread 4 leaving critical section
Thread 5 in critical section
Thread 5 leaving critical section
Thread 6 in critical section
Thread 6 leaving critical section
Thread 7 in critical section
Thread 7 leaving critical section
Thread 8 in critical section
Thread 8 leaving critical section
Thread 9 in critical section
Thread 9 leaving critical section
Thread 10 in critical section
Thread 10 leaving critical section
Event는 주로 작업의 완료를 알리거나 각 스래드에 브로드캐스트를 하기 위해 사용됩니다. 위 예시는 순수히 관련 함수를 사용한 것일 뿐이니 참고 부탁드립니다.
Timer
이 객체는 일정 시간이 지난 후 Singnaled 상태로 설정되는 특징을 가진 동기화 객체입니다. 주로 스레드가 일정시간 대기하거나 주기적인 작업을 해야 하는 경우 사용합니다. Event와 동일하게 Manual-reset, Auto-reset 방식이 있으며 Manual-reset은 Signaled 상태를 명시적으로 재설정하기 전까지 유지합니다. Auto-reset은 Signaled 상태가 되면 자동으로 Non-Signaled 상태로 돌아갑니다. 또한 주기적 알람 혹은 단발성 알람을 보낼 수 있습니다.
Timer는 아래의 함수들을 사용하여 제어할 수 있습니다.
HANDLE CreateWaitableTimer(
[in, optional] LPSECURITY_ATTRIBUTES lpTimerAttributes,
[in] BOOL bManualReset,
[in, optional] LPCWSTR lpTimerName
);
이 함수는 이름 있는 혹은 익명 Timer 객체를 생성합니다. 반환값은 생성한 Timer 객체의 HANDLE입니다. 매개변수는 아래와 같습니다.
- lpTimerAttributes: 보안객체입니다.
- bManualReset: 생성할 Timer의 모드를 설정합니다. TRUE면 Manual-reset FALSE면 Auto-reset 방식입니다.
- lpTimerName: 생성할 Timer의 이름입니다. 익명 Timer로 생성할 경우 NULL을 설정합니다.
BOOL SetWaitableTimer(
[in] HANDLE hTimer,
[in] const LARGE_INTEGER *lpDueTime,
[in] LONG lPeriod,
[in, optional] PTIMERAPCROUTINE pfnCompletionRoutine,
[in, optional] LPVOID lpArgToCompletionRoutine,
[in] BOOL fResume
);
이 함수는 Timer 객체를 설정하고 활성화합니다. 반환값은 함수의 성공/실패 여부입니다. 매개변수들은 아래와 같습니다.
- hTimer: 설정할 Timer 객체의 HANDLE입니다.
- lpDueTime: 알람이 발생하는 시간입니다. 즉 Timer가 Signaled 상태가 되는 시간입니다. 양수면 절대시간을 뜻하며 음수는 상대시간을 의마하게 됩니다. 시간 단위는 100 Nanoseconds입니다.
- lPeriod: 주기적 알람이 울리는 시간 간격입니다. 0을 넣으면 단발성 알람이 됩니다. 시간 단위는 Milliseconds입니다.
- pfnCompletionRoutine: Timer가 신호를 받을 때 실행할 콜백 함수입니다.
- lpArgToCompletionRoutine: 콜백 함수에 매개변수로 전달될 구조체 포인터입니다.
- fResume: 전원 보존 모드 복원 여부입니다. 대부분은 FALSE입니다.
BOOL CreateTimerQueueTimer(
[out] PHANDLE phNewTimer,
[in, optional] HANDLE TimerQueue,
[in] WAITORTIMERCALLBACK Callback,
[in, optional] PVOID Parameter,
[in] DWORD DueTime,
[in] DWORD Period,
[in] ULONG Flags
);
이 함수는 TimerQueue Timer를 생성합니다. 이 Timer는 지정된 초기시간을 대기한 다음 이후부터는 반복시간을 대기합니다. 대기가 만료되면 매개변수로 전달된 콜백함수를 실행합니다. 반환값은 함수의 성공/실패 여부입니다. 매개변수들은 아래와 같습니다.
- phNewTimer: 생성된 Timer를 받을 포인터입니다.
- TimerQueue: TimerQueue의 HANDLE입니다. NULL로 설정하면 기본 TimerQueue와 연결됩니다.
- Callback: Timer가 만료될 때 실행될 콜백 함수입니다.
- Parameter: 콜백 함수에 전달된 매개변수입니다.
- DueTime: 처음으로 Signaled가 되기 전 대기하는 시간입니다. 시간 단위는 Milliseconds입니다.
- Period: Signaled 상태가 된 이후 주기적으로 대기할 시간입니다. 0으로 하면 단발성 알람이 됩니다. 시간 단위는 Milliseconds입니다.
- Flags: 다양한 옵션을 설정할 수 있는 Flag입니다.
아래는 CreateWaitableTimer와 SetWaitableTimer을 사용하여 단발성 신호를 보내는 Timer를 간단히 사용해 보는 예시입니다.
#include <windows.h>
#include <iostream>
int main() {
HANDLE hTimer = CreateWaitableTimer(nullptr, TRUE, nullptr);
LARGE_INTEGER liDueTime;
liDueTime.QuadPart = -3 * 10'000'000LL;
SetWaitableTimer(hTimer, &liDueTime, 0, nullptr, nullptr, FALSE);
std::cout << "Waiting 3 seconds...\n";
WaitForSingleObject(hTimer, INFINITE);
std::cout << "Timer signaled!\n";
CloseHandle(hTimer);
return 0;
}
Waiting 3 seconds...
Timer signaled!
Timer는 주로 주기적으로 해야하는 로그 기록 혹은 데이터 수집과 같은 반복적 작업에 사용됩니다. 또한 타임아웃에 사용할 수 있습니다.
'개발 > Windows' 카테고리의 다른 글
Windows 캐시와 가상 메모리 (0) | 2025.05.22 |
---|---|
Windows 스레드 풀 (0) | 2025.05.19 |
Windows 스레드 생성과 소멸 (0) | 2025.04.29 |
Windows 스레드 (0) | 2025.04.28 |
Windows 스택 프레임과 레지스터 (0) | 2025.04.09 |