| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Tags
- Behavior Design Pattern
- trie
- Strongly Connected Component
- map
- Binary Lifting
- ccw 알고리즘
- 비트필드를 이용한 dp
- PROJECT
- 비트마스킹
- 분리 집합
- Spin Lock
- DP
- 게임 서버 아키텍처
- 이분 탐색
- localstorage
- LCA
- 그래프 탐색
- 최소 공통 조상
- Github
- Express.js
- Prisma
- 자바스크립트
- 2-SAT
- JavaScript
- SCC
- 벨만-포드
- R 그래프
- Lock-free Stack
- 강한 연결 요소
- 트라이
Archives
- Today
- Total
dh_0e
[C++/Game Server] Spin Lock, Event Lock, Condition Variable 본문
Spin Lock
- lock을 얻을 수 있을 때까지 루프를 돌며 lock을 얻으려고 계속 시도(busy waiting)하는 lock
- std::mutex는 커널 모드 진입 등의 오버헤드가 있으나, SpinLock은 사용자 모드에서만 동작하기 때문에 오버헤드가 적음
- CPU 자원을 계속 소비한다는 단점이 있음
class SpinLock
{
public:
void lock() {
bool expected = false;
bool desired = true;
while (_locked.compare_exchange_strong(expected, desired) == false) {
expected = false;
this_thread::sleep_for(100ms); // chrono 헤더 필요
// this_thread::sleep_for(1000ms); // 너무 바쁘게 돌지 않도록 약간 양보, 실제 상황에서는 조정 필요
// 또는
// this_thread::yield();
// 컨텍스트 스위칭을 유도하여 CPU 양보
// this_thread::sleep_for(0ms); 랑 같은 의미
}
}
void unlock() {
_locked.store(false); // 원자적으로 잠금 해제
}
private:
atomic<bool> _locked = false; // CAS 기반 원자적 플래그
};
- this_thread::를 사용하기 위해 <chrono> 헤더 필요
- sleep_for(0ms) or yield()로 CPU 점유율을 줄이고 컨텍스트 스위칭 기회를 줄 수 있음
- sleep_for(0ms): 잠깐 쉬겠다
- yield(): 지금 당장 CPU 양보하겠다
- 단순 루프만 돌 경우 CPU 100%를 차지하게 됨
사용 예제
SpinLock spinLock;
int sum = 0;
void Add() {
for (int i = 0; i < 100'000; i++) {
lock_guard<SpinLock> guard(spinLock);
sum++;
}
}
void Sub() {
for (int i = 0; i < 100'000; i++) {
lock_guard<SpinLock> guard(spinLock);
sum--;
}
}
int main() {
thread t1(Add);
thread t2(Sub);
t1.join();
t2.join();
cout << "최종 결과: " << sum << endl;
return 0;
}
- SpinLock을 std::mutex처럼 사용하여 sum++, sum--의 임계 구역을 보호할 수 있음
SpinLock vs Mutex
| 항목 | SpinLock | std::mutex |
| 구현 방식 | 사용자 모드 (CAS 루프) | 커널 모드 진입 (OS 스케줄링) |
| 성능 | 경합 짧을 때 빠름 | 경합 길어지면 더 효율적 |
| CPU 사용량 | 높음 (busy waiting) | 낮음 (sleep, block) |
| 공정성 | 낮음 (양보 없음) | 높음 |
| 사용 상황 | 짧고 빠른 임계 영역 | 긴 임계 영역 또는 IO 포함 시 |
- 짧은 시간 동안만 잠금이 필요한 작업에선 SpinLock이 유리
- 경합이 심하거나 긴 작업에는 std::mutex가 훨씬 안정적
- std::this_thread::yield() or sleep_for(0ms)을 조합하여 CPU 낭비를 줄일 수 있음
- 과도한 spin은 오히려 성능을 저하시킬 수 있으므로 적절한 타이밍 조절이 필요함
Event Lock
- 스레드 간 동기화를 할 때 일반적으로는 mutex, condition_variable, semaphore 같은 동기화 도구를 사용하지만,
Windows API에서는 Event Object를 사용한 동기화도 매우 중요하고 강력한 방식 중 하나 - 이벤트는 커널 오브젝트의 일부이며, Usage Count는 현재 오브젝트를 참조하고 있는 핸들의 수를 말함
mutex m;
queue<int32> q;
HANDLE handle; // Windows Event Object
// 이벤트 오브젝트 생성
handle = ::CreateEvent(
NULL, // 보안 속성: 기본값
FALSE, // bManualReset: 자동 리셋 (FALSE)
FALSE, // bInitialState: 초기 상태는 Non-Signaled (빨간불)
NULL // 이름 없음
);
- <windows.h> 헤더가 필요함
- bManualreset: 자동 리셋(false)으로 WaitForSignalObject가 깨어나면 자동으로 다시 빨간불(Non-Signaled)로 바뀜
- bInitialState: 시작 시 대기 상태를 나타냄 false(Non-Signaled) / true(Signal)
void Producer()
{
while (1)
{
{
unique_lock<mutex> lock(m);
q.push(100); // 데이터 생산
}
::SetEvent(handle); // 🔵 이벤트를 Signal 상태로 설정 (파란불)
this_thread::sleep_for(100ms);
}
}
void Consumer()
{
while (1)
{
::WaitForSingleObject(handle, INFINITE); // 🔴 기다림 → 🔵 되면 깨움
// 자동 리셋이므로 깨어나는 순간 다시 Non-Signaled 상태로 됨
unique_lock<mutex> lock(m); // 생산자와 소비자 간의 mutex
if (!q.empty()) {
int32 data = q.front();
q.pop();
cout << data << endl;
}
}
}
- Producer() 스레드에서 데이터를 큐에 넣은 뒤, 이벤트를 Signal(파란불) 상태로 바꿔서 소비자를 깨움
- Consumer() 스레드의 WaitForSignalObject가 이벤트가 Signal(파란불)일 때까지 대기하다가 Signal 상태가 되면 깨어나서 큐에 접근
- 자동 리셋 상태이므로 깨어난 스레드는 바로 다시 이벤트를 Non-Signal(빨간불) 상태로 바꿈
- 수동 리셋 상태일 경우 ::ResetEvent()를 수동으로 호출하여 빨간불로 전환해야 함
int main()
{
handle = ::CreateEvent(NULL, FALSE, FALSE, NULL);
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
::CloseHandle(handle);
return 0;
}
- CloseHandle() 함수로 이벤트 오브젝트 정리
Condition Variable
- <mutex> 헤더에 정의된 User-Level 동기화 객체 (커널 오브젝트가 아닌 경량 동기화 수단)
- 커널 오브젝트가 아니기 때문에 동일 프로세스 내에서만 사용 가능
- 항상 std::mutex 또는 std::unique_lock<mutex>와 함께 사용됨
- mutex만 사용하면 조건이 충족되지 않아도 lock을 잡으려는 CPU 낭비를 방지
- wait()로 조건이 만족될 때까지 스레드를 잠재우고, 다른 스레드가 신호를 줄 때만 깨어나게 해줌
wait() 내부 로직
- wait는 lock을 잠시 풀고, 대기 상태에 진입
- notify_one() or notify_all()로 깨어남
- 조건(predicate에 입력한 함수 or 람다)을 직접 확인
경우에 따라서 wait 함수 안에서 lock을 풀어줘야 하기 때문에 lockGuard가 아닌 unique_lock을 받음
사용 예시
mutex m;
condition_variable cv;
queue<int32_t> q;
void Producer() {
while (1) {
{
unique_lock<mutex> lock(m); // 1. 락 획득
q.push(100); // 2. 공유 자원 수정
} // 3. 락 해제
cv.notify_one(); // 4. 대기 중인 소비자 하나 깨움
this_thread::sleep_for(100ms);
}
}
void Consumer() {
while (1) {
unique_lock<mutex> lock(m); // 1. 락 획득
cv.wait(lock, [] { return !q.empty(); }); // 2. 조건 만족까지 대기
// while(!q.empty())cv.wait(lock); 과 같지만 가짜 깨어남(Spurious Wakeup)을 방지하지 못하는 Old(노가다) 방식
// 3. 조건 만족 시 큐에서 데이터 꺼냄
int32_t data = q.front();
q.pop();
cout << data << endl;
}
}
int main() {
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
return 0;
}
| 단계 | 스레드 | 설명 |
| A | Producer | lock(m)으로 락 획득 → q.push(100) → 락 해제 |
| B | Producer | notify_one() 호출 → Consumer가 대기 중이면 깨움 |
| C | Consumer | lock(m)으로 락 획득 후 wait() 진입 |
| D | Consumer | 조건이 false면 → lock.unlock() → 대기 상태 진입 |
| E | Consumer | notify를 받으면 → 다시 lock.lock() → 조건 재확인 후 처리 |
시나리오 1: "정상적인 순서" (A → C → D → B → E)
- Consumer가 먼저 wait()에 진입해 대기 중
- Producer가 q.push() 후 notify_one() 호출
- Consumer가 깨어남 → 조건 확인 → 처리
- notify가 씹히지 않고 정확하게 작업을수행
시나리오 2: "Producer가 먼저 notify" (A → B → C)
- Consumer가 wait()에 진입하기 전에 Producer가 notify_one()을 호출
- Consumer가 나중에 wait() 진입
- notify_one()은 그 순간 wait 중인 스레드에만 유효하므로 나중에 wait()에 들어간 스레드는 한 번 씹힐 수 있음
Spurious Wakeup (가짜 기상)
cv.wait(lock);
- 다음과 같이 predicate 없는 wait 함수는 notfiy가 위와 같이 한 번 씹히면 무한 sleep(deadlock) 위험이 있음
- wait()은 가끔 조건이 만족되지 않아도 깨어나는 경우가 있음
- 그래서 반드시 predicate (람다 또는 조건식)를 통해 조건을 재확인해야 함
condition_variable vs Windows Event
| 항목 | condition_variable | Windows Event (HANDLE) |
| 커널 오브젝트 여부 | ❌ (User-level) | ✅ 커널 오브젝트 |
| 락 필요 여부 | ✅ 락과 항상 함께 사용됨 | ❌ 이벤트 단독 사용 가능 |
| 스레드 간 알림 범위 | 동일 프로세스 내에서만 | 프로세스 간(IPC)도 가능 |
| 리셋 방식 | predicate로 자체 체크 | Auto / Manual 리셋 필요 |
'C++ > Game Server' 카테고리의 다른 글
| [C++/Game Server] 동기화 방식과 메모리 정책 (memory_order) (1) | 2026.02.04 |
|---|---|
| [C++/Game Server] future, promise, packaged_task (1) | 2025.08.08 |
| [C++/Game Server] Spin Lock, Volatile 변수 (1) | 2025.07.26 |
| [C++/Game Server] mutex, atomic, lock_guard, unique_lock (2) | 2025.07.24 |
| [C++/Game Server] Thread (1) | 2025.07.23 |