| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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
- ccw 알고리즘
- localstorage
- Binary Lifting
- 트라이
- map
- Github
- 자바스크립트
- 벨만-포드
- 이분 탐색
- 비트필드를 이용한 dp
- Spin Lock
- 그래프 탐색
- 최소 공통 조상
- 비트마스킹
- DP
- Express.js
- 강한 연결 요소
- LCA
- 분리 집합
- 2-SAT
- Strongly Connected Component
- 게임 서버 아키텍처
- SCC
- Behavior Design Pattern
- Prisma
- trie
- JavaScript
- PROJECT
- Lock-free Stack
- R 그래프
Archives
- Today
- Total
dh_0e
[C++/Game Server] Lock-Free Stack (with Smart Pointer, Reference Counting) 본문
C++/Game Server
[C++/Game Server] Lock-Free Stack (with Smart Pointer, Reference Counting)
dh_0e 2026. 2. 10. 14:56Lock-Free Stack with Smart Pointer
- Garbage Collector의 기능을 스마트 포인터로 대체하는 방법
- 사실 스마트 포인터가 내부적으로 lock을 사용하기 때문에 엄밀히 말해서 Lock-Free 방식은 아님
- shared_ptr<int> ptr;
atomic_is_lock_free(&ptr); 로 CPU가 lock을 사용하는지 검사해 보면 false 나옴
- shared_ptr<int> ptr;
template<typename T>
class LockFreeStack
{
struct Node {
Node(const T& value) : data(make_shared<T>(value)), next(nullptr) {}
shared_ptr<T> data;
shared_ptr<Node> next;
};
public:
void Push(const T& value)
{
shared_ptr<Node> node = make_shared<Node>(value);
node->next = std::atomic_load(&_head);
while (std::atomic_compare_exchange_weak(&_head, &node->next, node) == false) // 기능은 똑같은데 C언어 스타일의 전역 함수
{
//node->next = _head; // 함수 안에서 자동으로 실행됨 (이 값이 바뀌었으니까 다시 알려줄게)
}
}
shared_ptr<T> TryPop()
{
shared_ptr<Node> oldHead = std::atomic_load(&_head);
while (oldHead && std::atomic_compare_exchange_weak(&_head, &oldHead, oldHead->next) == false)
{
}
if (oldHead == nullptr)
return shared_ptr<T>();
return oldHead->data;
}
private:
shared_ptr<Node> _head;
};
Lock-Free Stack with Reference Counting
- 이중 참조 횟수 기록(Split Reference Counting) 기법을 활용한 Lock-Free Stack
- 단순히 head 포인터만 CAS로 바꾸는 방식이 아니라, 메모리 해제 시점의 안정성을 확보하기 위해 외부 참조 횟수(External Count)와 내부 참조 횟수(Internal Count)를 나누어 관리하는 것이 핵심임
- CountedNodePtr(외부 참조 함수): head에 직접 붙어 있는 참조 횟수로, 현재 이 Node가 head로서 얼마나 많은 스레드에 의해 읽히고(참조되고) 있는가를 나타냄
- IncreaseHeadCount() 함수에서 노드에 접근하기 전에 일단 externalCount를 1 올리고 시작하는 것은 "내가 지금 이 노드를 보고 있으니 아무도 지우지 마!"라고 선언하는 것과 같음
- internalCount(내부 참조 횟수): Node 구조체 안에 위치한 참조 횟수로, "노드가 head에서 떨어져 나온 뒤, 아직 이 노드를 붙잡고 남은(참조권은 얻었지만 소유권은 얻지 못한) 스레드가 몇 개인가"를 나타냄
- Trypop에서 소유권 획득에 성공한 스레드는 externalCount에서 자신이 점유했던 몫을 빼고, 그 나머지를 internalCount에 더해줌
- 삭제 조건: $externalCount + internalCount = 0$
- fetch_add의 결괏값이 특정 조건(-countIncrease)과 일치할 때 delete ptr;을 수행하여 마지막으로 노드를 나가는 스레드가 청소를 담당하게 설계하였음
- CountedNodePtr(외부 참조 함수): head에 직접 붙어 있는 참조 횟수로, 현재 이 Node가 head로서 얼마나 많은 스레드에 의해 읽히고(참조되고) 있는가를 나타냄
template<typename T>
class LockFreeStack
{
struct Node;
struct CountedNodePtr
{
int32 externalCount = 0; // 32bit
Node* ptr = nullptr; // 64bit
// 구조체를 64bit 크기로 맞춰서 lock-free가 되게끔 조절하는 꼼수가 필요함
// lock-free가 된다고 가정하고 코드를 작성
};
struct Node {
Node(const T& value) : data(make_shared<T>(value))
{
}
shared_ptr<T> data;
atomic<int32> internalCount = 0;
CountedNodePtr next;
};
public:
void Push(const T& value)
{
CountedNodePtr node;
node.ptr = new Node(value);
node.externalCount = 1;
node.ptr->next = _head;
while (_head.compare_exchange_weak(node.ptr->next, node) == false)
{
}
}
shared_ptr<T> TryPop()
{
CountedNodePtr oldHead = _head;
while (1)
{
// 참조권 획득 (externalCount를 현 시점 기준 +1한 스레드가 이김)
IncreaseHeadCount(oldHead);
// 최소 externalCount >= 2 일테니 삭제 X (안전하게 접근 가능 상태)
Node* ptr = oldHead.ptr;
if (ptr == nullptr)return shared_ptr<T>();
// 소유권 획득 (head를 ptr->next로 바꿔치긱 한 애가 이김 == externalCount까지 똑같아야 함)
if (_head.compare_exchange_strong(oldHead, ptr->next))
{
shared_ptr<T> res;
res.swap(ptr->data);
// external : 1
// internal : 0
// 나 말고 또 누가 있는가?
const int32 countIncrease = oldHead.externalCount - 2;
// fetch_add는 더해주고 이전의 값을 뱉어줌 a++ 같은 거
if (ptr->internalCount.fetch_add(countIncrease) == -countIncrease)
delete ptr;
return res;
}
// 소유권 획득에 실패한 스레드중 마지막 스레드가 나가기 전에 delete 실행
else if (ptr->internalCount.fetch_sub(1) == 1)
{
// 참조권은 얻었으나, 소유권은 실패 -> 뒷수습 직접 진행
delete ptr;
}
}
}
private:
void IncreaseHeadCount(CountedNodePtr& oldCounter)
{
// Speen Lock 느낌으로 참조할 권리 다들 획득 시도 중
while (1)
{
CountedNodePtr newCounter = oldCounter;
newCounter.externalCount++;
// externalCount를 1 증가 시켜서 head로 넣으려고 시도
if (_head.compare_exchange_strong(oldCounter, newCounter))
{
oldCounter.externalCount = newCounter.externalCount; // 증가된 externalCount를 oldHead에도 저장 후 탈출
break;
}
}
}
private:
atomic<CountedNodePtr> _head;
// 진짜 lock-free인지? is_lock_free로 봐봐야 함
};
- 어렵다.. lock-free queue는 더 어렵다는데 나중에 공부해야겠다
'C++ > Game Server' 카테고리의 다른 글
| [C++/Game Server] Reader-Writer Lock (0) | 2026.02.12 |
|---|---|
| [C++/Game Server] Thread Manager (+각 서버 file 설명) (0) | 2026.02.11 |
| [C++/Game Server] Lock-Based Stack/Queue (+delete, IN/OUT) (0) | 2026.02.05 |
| [C++/Game Server] Thread Local Storage (0) | 2026.02.05 |
| [C++/Game Server] 동기화 방식과 메모리 정책 (memory_order) (1) | 2026.02.04 |