오구의코딩모험

[Server] 메모리 모델, Thread Local Storage (= TLS), Lock-Based Stack/Queue 본문

Game/Server

[Server] 메모리 모델, Thread Local Storage (= TLS), Lock-Based Stack/Queue

오구.cpp 2025. 3. 31. 15:40
반응형

 

메모리 모델, TLS, 락 기반 자료구조 학습 정리

 

 

이번엔 C++ 멀티스레드 프로그래밍에서 중요한 개념들인 Memory Model, Thread Local Storage, atomic, 그리고 Lock-Based 자료구조(스택/큐)를 공부했습니다.

 


 

1. 메모리 모델 (Memory Order)

 

Memory Model (정책)
1) Sequentially Consistent (seq_cst)
2) Acquire-Release (acquire, release)
3) Relaxed (relaxed)

(1) seq_cst (가장 엄격 = 컴파일러 최적화 여지 적음 = 직관적)
  - 가시성 문제 바로 해결! 코드 재배치 바로 해결!

(2) acquire-release
 - 딱 중간!
 - release 명령 이전의 메모리 명령들이, 해당 명령 이후로 재배치 되는 것을 금지
 - 그리고 acquire로 같은 변수를 읽는 쓰레드가 있다면
 - release 이전의 명령들이 -> acquire 하는 순간에 관찰 가능 (가시성 보장)

(3) relaxed (자유롭다 = 컴파일러 최적화 여지 많음 = 직관적이지 않음)
 - 코드 재배치도 멋대로 가능! 가시성 해결 NO!
 - 가장 기본 조건 (동일 객체에 대한 동일 관전 순서만 보장)

* 인텔, AMD의 경우 애당초 순차적 일관성을 보장을 해서 seq_cst를 써도 별다른 부하가 없음
* ARM의 경우 꽤 차이가 있다고 한다!

 

 

 

memory_order 종류 요약

  • seq_cst : 가장 강력한 일관성 보장, 코드 재배치 없음, 컴파일러 최적화 거의 못함 (직관적)
  • acquire/release : 적절한 수준의 최적화 허용, 필요한 시점만 제한
  • relaxed : 최대의 성능, 그러나 동기화 보장 없음 (복잡한 상황에 적합)

 

 

예시 코드

atomic<bool> ready;
int32 value;

void Producer()
{
    value = 10;
    ready.store(true, memory_order::release); // 이후 명령 재배치 방지
}

void Consumer()
{
    while (ready.load(memory_order::acquire) == false)
        ;
    // 여기 도달했을 땐 value = 10 이 보장됨
}

 

 


 

 

2. atomic 연산자들

 

기본적인 사용 예시

atomic<bool> flag = false;

// 값을 교환
bool old = flag.exchange(true);

// 조건부 교체 (Compare-And-Swap)
bool expected = false;
bool desired = true;
bool success = flag.compare_exchange_strong(expected, desired);

 

 

  • exchange() : 값 바꾸고 이전 값 리턴
  • compare_exchange_strong() : 조건 맞으면 값 교체, 실패 시 expected가 최신 값으로 갱신됨

 


 

 

3. Thread Local Storage (TLS)

  • thread_local을 사용하면 스레드마다 독립적인 전역 변수를 가질 수 있다.
  • 스레드 ID나 로컬 상태 저장 등에 유용

 

예시 코드

thread_local int32 LThreadId = 0;

void ThreadMain(int32 threadId)
{
    LThreadId = threadId;

    while (true)
    {
        cout << "Hi I am Thread " << LThreadId << endl;
        this_thread::sleep_for(1s);
    }
}

int main()
{
    vector<thread> threads;
    for (int32 i = 0; i < 10; ++i)
        threads.push_back(ThreadMain, i + 1);

    for (thread& t : threads)
        t.join();
}

 

 

 


 

 

4. Lock-Based Stack / Queue

 

 

<Stack>

template<typename T>
class LockStack
{
public:
    void Push(T value)
    {
        lock_guard<mutex> lock(_mutex);
        _stack.push(std::move(value));
        _condVar.notify_one();
    }

    bool TryPop(T& value)
    {
        lock_guard<mutex> lock(_mutex);
        if (_stack.empty()) return false;
        value = std::move(_stack.top());
        _stack.pop();
        return true;
    }

    void WaitPop(T& value)
    {
        unique_lock<mutex> lock(_mutex);
        _condVar.wait(lock, [this] { return !_stack.empty(); });
        value = std::move(_stack.top());
        _stack.pop();
    }

private:
    stack<T> _stack;
    mutex _mutex;
    condition_variable _condVar;
};

 

 

 

<Queue>

template<typename T>
class LockQueue
{
public:
    void Push(T value)
    {
        lock_guard<mutex> lock(_mutex);
        _queue.push(std::move(value));
        _condVar.notify_one();
    }

    bool TryPop(T& value)
    {
        lock_guard<mutex> lock(_mutex);
        if (_queue.empty()) return false;
        value = std::move(_queue.front());
        _queue.pop();
        return true;
    }

    void WaitPop(T& value)
    {
        unique_lock<mutex> lock(_mutex);
        _condVar.wait(lock, [this] { return !_queue.empty(); });
        value = std::move(_queue.front());
        _queue.pop();
    }

private:
    queue<T> _queue;
    mutex _mutex;
    condition_variable _condVar;
};

 

 

 

<활용 예제>

LockQueue<int32> q;

void Push()
{
    while (true)
    {
        int32 value = rand() % 100;
        q.Push(value);
        this_thread::sleep_for(10ms);
    }
}

void Pop()
{
    while (true)
    {
        int32 data = 0;
        if (q.TryPop(OUT data))
            cout << data << endl;
    }
}

int main()
{
    thread t1(Push);
    thread t2(Pop);
    thread t3(Pop);

    t1.join();
    t2.join();
    t3.join();
}

 

 

  • WaitPop 함수는 좀 더 유연한 잠금과 해제가 필요하다. (값이 들어왔을 때 해제, 값이 들어올 때까지 대기)
  • 따라서 WaitPop은 unique_lock을 사용한다.
  • Push / TryPop 함수는 스코프를 벗어나면 자동으로 뮤텍스를 해제해야 하므로 lock_guard를 사용한다.

 


 

정리

 

memory_order

- 대개 seq_cst를 사용하는 것이 일반적이다.

- release / acquire 조합을 잘 써도 퍼포먼스와 안정성 둘 다 챙길 수 있다.

 

atomic 연산

- exchange, compare_exchange_*는 thread-safe한 값 교체에 매우 유용

 

TLS

- 전역변수를 스레드별로 분리할 수 있다. thread_local 한 줄이면 끝!

 

LockStack / LockQueue

- TryPop은 논블로킹, WaitPop은 대기 방식 → 상황에 맞게 사용

 

condition_variable

- 반드시 조건을 람다로 넣고 while로 감싸자 (spurious wakeup 방지)

반응형
Comments