티스토리 뷰
World - Render 스레드 분리 및 CommandList 설계
현재 프로토타입에서 World(Update)와 Renderer 스레드를 분리하고,
데이터 동기화 과정에서 Mutex를 제거한 설계 근거를 설명합니다.
1. 개요 (Background)
초기에서는 World 스레드(객체의 상태 변경)와 Renderer 스레드(화면 출력)가 동일한 CommandList 자원에
접근할 때 발생할 수 있는 데이터 경합(Race Condition)을 방지하기 위해 Mutex 도입을 고려했습니다.
하지만 Mutex의 경우에는 런타임 중에 계속해서 World와 Renderer 스레드 간의 대기 시간(Lock)을
유발할 것으로 예상되었습니다.
2. 핵심 설계 결정: CommandList 소유권 분리 (Lock-Free)
구현한 방식은 "스레드 간 자원 공유가 아닌, 자원의 소유권 이전(Ownership Transfer)" 모델입니다.
논의했던 CommandList의 생명 주기와 스레드 간 소유권 전이를 상태 중심으로 정리한 표입니다.
이 설계의 핵심은 각 상태에서 특정 스레드만 자원에 접근할 수 있도록 제한하여
Mutex 없이도 데이터 안전성을 보장하는 데 있습니다.
2.1 Mutex를 제거하게 된 논리
- 기존 고민
CommandList 하나에 여러 스레드가 동시에 Push 혹은 Pop를 호출하면
데이터가 깨질 수 있으므로 락(Lock)이 필요하다고 생각함. - 최종 해결책
World 전용 CommandList와 Renderer 전용 CommandList를 물리적으로 분리 및 상태 전이 - 메커니즘
상태 전이를 통한 World와 Render의 중복되지 않는 접근으로 Mutex 없이 CommandList 공유 가능
| 상태 | 설명 | 주체 (Owner) | World 접근 | Renderer 접근 |
|---|---|---|---|---|
| eFree | 렌더링 완료, 기록 대기 중 | None (Pool) | 가능 | 불가능 |
| eWriting | 기록 진행 중 | World | 사용 중 | 불가능 |
| eReady | 기록 완료, 렌더링 대기 중 | None (Pool) | 불가능 | 가능 |
| eReading | 렌더링 진행 중 | Renderer | 불가능 | 사용 중 |
2.2 설계 포인트 요약
- 배타적 소유권
표에서 보시듯 World와 Renderer가 동시에 '가능' 상태가 되는 구간이 없습니다.
이를 통해 물리적인 Lock 없이 로직상으로 데이터 경합을 완전히 차단합니다. - 순환 구조
eReading이 끝난 CommandList는 다시 eFree 상태로 돌아가 리소스 풀(Pool)에 반납되며,
다음 프레임에서 재사용됩니다. - 상태 전이 시점
eFree -> eWriting : CommandPool로부터 World에Acquire()호출 시
eWriting -> eReady : World에서 기록 완료 후, CommandPool로Release()호출하여 반납 시
eReady -> eReading : CommandPool로부터 Renderer가Acquire()호출 시
eReading -> eFree : Renderer에서 렌더링 완료 후, CommandPool로Release()호출하여 반납시
2.3 이 설계의 이점
- 경합 제거 (Lock-Free)
CommandList자체에Mutex를 걸지 않아도 되므로, 컨텍스트 스위칭 비용과 대기 시간이 사라집니다. - 병렬성 극대화
World가 물리/로직 연산을 하는 동시에Renderer는 독립적으로 GPU 명령을 실행할 수 있습니다.
3. 구현 중 마주친 문제점 및 해결 (Troubleshooting)
3.1 스레드 속도 차이에 따른 동기화 문제
두 스레드의 처리 속도가 불일치할 때, 한정된 자원인 CommandPool 내의
CommandList가 고갈되거나 렌더링할 데이터가 없는 현상이 발생했습니다.
- 현상
- World가 Renderer보다 빠를 경우
World가 계속 새로운CommandList를 생성하여CommandPool의 제한 수치를 초과하거나,Renderer가 미처 처리하지 못한 데이터가 쌓여, 프레임이 밀리는 이슈가 발생함. - Renderer가 World보다 빠를 경우:
Renderer가 GPU에 명령을 보내야 하는데,World로부터 전달받은eReady상태의CommandList가 없어 렌더링 파이프라인이 멈춤.
- World가 Renderer보다 빠를 경우
- 해결:
- 상호 프레임 대기(Wait) 로직 도입
- World Wait
World와Renderer간의 프레임 격차가CommandPool의 제한된 용량(Buffer Size)보다 커질 경우,World스레드를Wait시킵니다. 이는Renderer가 최소 하나 이상의CommandList를 실행하고eFree로 반납할 때까지 지속됩니다. - Renderer Wait
Renderer리소스 풀(Pool)를 확인했을 때 사용할 수 있는eReady리스트가 없다면,World가 현재 기록 중인(eWriting) 리스트를 마칠 때까지Renderer를Wait시킵니다.
- World Wait
- 상호 프레임 대기(Wait) 로직 도입
- 결과
무분별한 프레임 생략(Skipping) 대신 안정적인 흐름 제어(Flow Control)를 통해 프레임 지연을
일정 수준으로 유지했습니다.
3.2 데이터 스냅샷(Snapshot)의 필요성
- 문제
World가 객체 위치를 계속 바꾸고 있는데Renderer가 해당 객체의 포인터를 직접 참조하면,
렌더링 도중 위치 값이 변해서 일부 오브젝트가 잘못된 위치에 그려지는 현상 발생 - 해결
CommandList에 명령을 기록할 때, 해당 시점의 상태(위치, 회전 등)를 값(Value)으로 복사하여
Renderer가 World의 상태 변화에 영향을 받지 않도록 격리했습니다.
4. 요약 및 향후 과제
현재 프로토타입은 "World가 쓸 때는 Renderer가 안 쓰고, Renderer가 쓸 때는 World가 안 쓴다"는
단순하면서도 강력한 소유권 분리 원칙을 통해 Mutex 없는 구조를 달성했습니다.
- 현재 상태
CommandList교체(Swap) 시점에만 아주 짧은 원자적(Atomic) 연산 혹은 최소한의 동기화만 발생함. - 향후 과제
여러 개의 Worker Thread가 동시에 각자의 CommandList를 생성하고,
이를 하나로 병합하는Parallel Command Recording구조로 확장 가능성을 검토 중.
프로토 타입 코드
#include <stdio.h>
#include <atomic>
#include <thread>
#include <vector>
#include <algorithm>
#include <memory>
#include <condition_variable>
#include <Windows.h>
#include <random>
#include <random>
#include <chrono>
void RandomSleep(int minMs, int maxMs) {
static std::random_device rd;
static std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(minMs, maxMs);
int sleepTime = dis(gen);
std::this_thread::sleep_for(std::chrono::milliseconds(sleepTime));
}
class CommandQueue
{
public :
enum class eState
{
eFree = 0x00,
eWriting = 0x01,
eReady = 0x02,
eReading = 0x03
};
CommandQueue()
: m_eState(eState::eFree)
, m_buffer()
{}
std::vector<std::string>& GetBuffer()
{
return m_buffer;
}
eState GetState() const
{
return m_eState.load();
}
void SetState(const eState state)
{
m_eState = state;
}
size_t GetFrame() const
{
return m_frame;
}
void SetFrame(const size_t frame)
{
m_frame = frame;
}
private :
size_t m_frame = 0;
std::atomic<eState> m_eState;
std::vector<std::string> m_buffer;
};
class CommandPool
{
public :
enum class eType
{
eProceduer,
eConsumer
};
public :
CommandQueue& Acquire(eType type)
{
if (eType::eProceduer == type)
{
{
std::unique_lock<std::mutex> lock(m_mutexPro);
m_cvProceduer.wait(lock, [this]()
{
for (auto& queue : this->m_commandQueue)
{
if (queue.GetState() == CommandQueue::eState::eFree)
{
return true;
}
}
return false;
});
}
for (auto& queue : m_commandQueue)
{
if (queue.GetState() == CommandQueue::eState::eFree)
{
queue.SetState(CommandQueue::eState::eWriting);
return queue;
}
}
}
else
{
{
std::unique_lock<std::mutex> lock(m_mutexCon);
m_cvConsumer.wait(lock, [this]()
{
for (auto& queue : this->m_commandQueue)
{
if (queue.GetState() == CommandQueue::eState::eReady)
{
return true;
}
}
return false;
});
}
size_t minFrame = size_t(0) - 1;
size_t minIndex = size_t(0) - 1;
for (size_t index = 0; index < 3; index++)
{
auto& queue = m_commandQueue[index];
if (queue.GetState() == CommandQueue::eState::eReady && minFrame >= queue.GetFrame())
{
minIndex = index;
minFrame = queue.GetFrame();
}
}
return m_commandQueue[minIndex];
}
}
void Release(eType type, CommandQueue& queue)
{
if (eType::eProceduer == type)
{
queue.SetState(CommandQueue::eState::eReady);
m_cvConsumer.notify_one();
}
else
{
queue.SetState(CommandQueue::eState::eFree);
queue.GetBuffer().clear();
m_cvProceduer.notify_one();
}
}
private :
std::mutex m_mutexPro;
std::mutex m_mutexCon;
std::condition_variable m_cvProceduer;
std::condition_variable m_cvConsumer;
CommandQueue m_commandQueue[3];
};
class Worker
{
public :
Worker()
: m_thread()
, m_isRunning(false)
{}
void Start()
{
if (m_isRunning)
{
return;
}
else
{
m_isRunning = true;
m_thread = std::thread(&Worker::Run, this);
}
}
void Stop()
{
if (!m_isRunning)
{
return;
}
else
{
m_isRunning = false;
if (m_thread.joinable())
{
m_thread.join();
}
}
}
private :
virtual void Run()
{
while (m_isRunning)
{
onUpdate();
}
}
virtual void onUpdate() { printf("Update %d \n", GetCurrentThreadId()); };
private :
std::thread m_thread;
std::atomic<bool> m_isRunning;
};
class WorldWorker : public Worker
{
public :
WorldWorker(const std::shared_ptr<CommandPool>& commandPool)
: m_commandPool(commandPool)
, m_frame(0)
{}
void onUpdate() override
{
m_frame++;
if (m_commandPool)
{
CommandQueue& queue = m_commandPool->Acquire(CommandPool::eType::eProceduer);
queue.SetFrame(m_frame);
RandomSleep(10, 50);
queue.GetBuffer().push_back(std::string("Updated Frame :" + std::to_string(m_frame)));
printf("[World] Frame : %d \n", m_frame);
m_commandPool->Release(CommandPool::eType::eProceduer, queue);
}
}
private :
size_t m_frame;
std::shared_ptr<CommandPool> m_commandPool;
};
class RenderWorker : public Worker
{
public:
RenderWorker(const std::shared_ptr<CommandPool>& commandPool)
: m_commandPool(commandPool)
, m_frame(0)
{}
void onUpdate() override
{
m_frame++;
if (m_commandPool)
{
CommandQueue& queue = m_commandPool->Acquire(CommandPool::eType::eConsumer);
for (auto& command : queue.GetBuffer())
{
printf("[Renderer] Frame : %d | %s \n", m_frame, command.c_str());
}
RandomSleep(10, 100);
m_commandPool->Release(CommandPool::eType::eConsumer, queue);
}
}
private:
size_t m_frame;
std::shared_ptr<CommandPool> m_commandPool;
};
int main()
{
std::shared_ptr<CommandPool> commandPool = std::make_shared<CommandPool>();
WorldWorker world(commandPool);
RenderWorker render(commandPool);
world.Start();
render.Start();
Sleep(5000);
world.Stop();
render.Stop();
return 0;
}'개인 프로젝트 > Renderer' 카테고리의 다른 글
| [ Renderer ] UE5 - RHI 계층 구조 (0) | 2026.01.27 |
|---|---|
| [ Renderer ] RHICommand (0) | 2026.01.01 |
| [ Renderer ] wtr::Engine 구조 설계 (0) | 2025.12.26 |
- Total
- Today
- Yesterday
- GameEngine
- cmake
- Reflection
- GC
- cmakelists
- C++
- RHICommand
- 포인터
- void pointer
- 보이드 포인터
- 증명
- 내부단편화
- logproject
- pointer
- 그래픽스
- 세그먼테이션기법
- 모던 C++ 챌린지
- 외부단편화
- SFINAE #Template #C++
- RHI
- 페이징기법
- FloatingPoint
- 개발 일지
- void* pointer
- MemoryProject
- std::is_base_of
- 뮤택스
- 수학
- 로드리게스 회전 행렬
- 메모리
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |