삭제의 핵심은 분리를 미리하고 분리한 대상을 현재 스레드의 스택 메모리에 삭제할 주소를 지역변수로 들고 있다가
다시 자신의 스레드로 돌아왔을때 분리된 삭제할 메모리를 그때서야 삭제 하는 것이다
삭제하는 방식(룰)
- 데이터 분리
- count 체크
- 나 혼자면 삭제
void tryDelete(Node* oldHead)
{
나 외에 누가 있는지 먼저 체크
_popCount 는 atomic 이다 1보다 크면 누군가 tryPop 에 들어와 추가로 pop 을 하려고 하는 상황
if (_popCount == 1)
{
oldHead 를 삭제 할거긴한데 대기 삭제 리스트도 이때 한번 삭제 시도를 해보자는것
_pendingList 은 null 이 되고 _pendingList 포인터를 node 에 넣음
이로써 이 쓰레드에서 데이터를 분리해둔 상태가 되고, 다른스레드에서 이 작업을 한다 해도 별도의 스택영역
지역변수인 node 에 가져오려고 하는것이고 , _pendingList는 이미 현 스레드에서 null 이 됐음으로
현재 스레드의 node만 _pendingList 를 들고 있는 안전한 상태가 된다
즉 이렇게 꺼내도면 다른 쓰레드에서 push 해서 노드를 추가한다고 해도 이미 기존 _pendingList 들을 현재
스레드의 node 에 포인터를 꺼내왔고 _pendingList를 null 로 만들었기 때문에 아무 상관이 없다
Node* node = _pendingList.exchange(nullptr); //한번에 교환 처리 됨 atomic 임으로
atomic 으로 감소가 atomic 하게 한줄로처리 됨, 감소 처리하고 리턴 되는것은
복사 된 스택 임시 값과 == 0 의 비교 임으로 비교 자체는 다른 스레드와 영향 없음
if (--_popCount == 0)
{
그래서 -1 을 감소시켜서 0이라면 중간에 끼어든 애가 없다는 것
끼어든 애가 없음 -> 삭제 진행
만약 if (--_popCount == 0) 이 구문이 지나고 if 문 안으로 들어와서 다른스레드가 _popCount를 증가 시켰어도
이미 미리 node 값으로 _pendingList 을 빼 왔고 지역 변수 node로 스레드 분리된 값으로 pendingList 를 들고
있어서 안전함 => 삭제 가능
그래서 분리 시켜놨기 때문에 이 구문안에서 삭제 처리를 진행 할 수 있다
DeleteNodes(node);
}
else if(node)
{
다른 스레드로 누군가 tryDelete 을 또 호출 했기 때문에 누가 끼어들었으니 다시 갖다 놓아야 함
ChainPendingNodeList(node); //node 를 _pendlingList 에 추가해 놓는 처리
}
혼자서 pop 을 할려고 하는것이긴 한데
이 안에 들어 왔을 때 바로 delete를 수행하는데 만약 if 문 이 구문 안에 현 스레드가 들어 왔을때
tryPop 함수를 다른 스레드에서 또 호출하게 된다면? 상황이 복잡해 질것 같지만
이미 tryDelete 를 하기 전에 oldhead 노드를 따로 삭제할 노드 포인터로 빼놓았기 때문에
즉 현재 삭제하려고 한 oldhead 의 로컬 변수와, 새롭게 진입한 tryPop 스레드의 oldHead 는 서로 다른 로컬 변수이 기 때문에 서로 상관 없다 => 즉 안전하다
그래서 여기서 delete 를 하는게 가능하다
delete oldHead;
...
}
else
{
.....
#pragma once
#include <mutex>
template<typename T>
class LockStack
{
public :
LockStack() = default;
LockStack(const LockStack&) = delete;
LockStack& operator=(const LockStack&) = delete;
void push(T value)
{
lock_guard<mutex> lock(_mutex);
_stack.push( std::move(value));
_conVar.notify_one();
}
//데이터를 꺼낼때 여기에서 따로 구현하게 되는 Empty 함수를 호출 체크하는게 의미가 없는데, Empty 함수안에
// lock_guard 가 있고 Empty 함수가 끝난 이후 Pop 함수로 되돌아 갈때
// 다른 스레드에서 stack 에 데이터를 추가하거나 제거 하게 된다면 현재 스레드에서 pop 을 할때
//스택이 비어 있는지 차 있는지 알수 없음으로 (그 순간 사이에서) 무의미하다
//그래서 pop 할때 empty 함수를 호출하지 않고 내부에 구현한다, 유저 편의상 Empty 함수를 제공해 줄 순 있겠지만..
bool tryPop(T& value)
{
lock_guard<mutex> lock(_mutex);
if (_stack.empty())
{
return false;
}
value = std::move(_stack.top());
_stack.pop();
return true;
}
//pop 할 데이터가 없을때 대기하다가 pop 할 데이터가 생기면 그때 pop하고 끝내는 함수
void waitPop(T& value)
{
unique_lock<mutex> lock(_mutex);
// stack 이 비어있지 않다면(false) == false => true
// stack 이 비어 있다면 (true) == false => false , 조건이 만족하지 않으면 lock 을 풀고 sleep 하게 된다
_conVar.wait(lock, [this] { return _stack.empty() == false; });
value = std::move(_stack.top());
_stack.pop();
}
private :
stack<T> _stack;
mutex _mutex;
condition_variable _conVar;
};
template<typename T>
class LockFreeStack
{
struct Node
{
Node(const T& value) : data(value), next(nullptr){}
T data;
Node* next;
};
public :
/* 1. 새 노드를 만든다
2. 새 노드의 next = nead
3. head = 새노드 */
//node 추가는 head 바로 다음에 새로운 노드가 추가 되는 방식이다
void push(const T& value)
{
Node* node = new Node(value);
node->next = _head;
while (_head.compare_exchange_weak(node->next, node) == false) //atomic 으로 한번에 처리됨
{
//node->next = _head; //없어도 push 로직상 맞아 떨어짐
}
}
/* 1. head 읽기
2. head->next 읽기
3. head = dead->next
4. data 추출해서 반환
5. 추출한 노드를 삭제 */
bool tryPop(T& value)
{
//삭제를 시작하고 있는 스레드 카운트 증가
++_popCount;
Node* oldHead = _head;
/*
if (_head == oldHead)
{
_head = oldHead->next;
return true;
}
else
{
oldHead = _head;
return false;
}
*/
//이 줄이 실행 완료 되면 노드를 스택에서빼낸 상태 oldHead
while (oldHead && _head.compare_exchange_weak(oldHead, oldHead->next) == false) //atomic 으로 한번에 처리가 됨
{
// oldHead = _head;
}
if (oldHead == nullptr)
{
//삭제 할게 없다면 해당 삭제하고 있는 스레드 감소
--_popCount;
return false;
}
value = oldHead->data;
tryDelete(oldHead);
return true;
}
//삭제하는 방식(룰)
//1. 데이터 분리
//2. count 체크
//3 나 혼자면 삭제
void tryDelete(Node* oldHead)
{
//나 외에 누가 있는지 먼저 체크
if (_popCount == 1) //_popCount 는 atomic 이다 1보다 크면 누군가 tryPop 에 들어와 추가로 pop 을 하려고 하는 상황
{
Node* node = _pendingList.exchange(nullptr); //한번에 교환 처리 됨 atomic 임으로
if (--_popCount == 0) //atomic 으로 감소가 atomic 하게 한줄로처리 됨, 감소 처리하고 리턴 되는것은 복사 된 스택 임시 값과 == 0 의 비교 임으로 비교 자체는 다른 스레드와 영향 없음
{
//그래서 -1 을 감소시켜서 0이라면 중간에 끼어든 애가 없다는 것
//끼어든 애가 없음 -> 삭제 진행
//만약 if (--_popCount == 0) 이 구문이 지나고 if 문 안으로 들어와서 다른스레드가 _popCount를 증가 시켰어도
//이미 미리 node 값으로 _pendingList 을 빼 왔고 지역 변수 node로 스레드 분리된 값으로 pendingList 를 들고 있어서 안전함 => 삭제 가능
DeleteNodes(node);
}
else if(node)
{
//누가 끼어들었으니 다시 갖다 놓아야 함
ChainPendingNodeList(node);
}
//혼자서 pop 을 할려고 하는것이긴 한데
//이 안에 들어 왔을 때 바로 delete를 수행하는데 만약 if 문 이 구문 안에 현 스레드가 들어 왔을때
//tryPop 함수를 다른 스레드에서 또 호출하게 된다면? 상황이 복잡해 질것 같지만
// 이미 tryDelete 를 하기 전에 oldhead 노드를 따로 삭제할 노드 포인터로 빼놓았기 때문에
//즉 현재 삭제하려고 한 oldhead 의 로컬 변수와, 새롭게 진입한 tryPop 스레드의 oldHead 는 서로 다른 로컬 변수이기 때문에 서로 상관 없다 => 즉 안전하다
//그래서 여기서 delete 를 하는게 가능하다
delete oldHead;
}
else
{
//누가 있는 상황이니 그럼 지금 삭제 하지 않고, 삭제 예약만 해놓는다 => _pendingList 에 추가해 놓는다
ChainPendingNode(oldHead);
--_popCount;
}
}
void ChainPendingNode(Node* node)
{
ChainPendingNodeList(node, node);
}
//first 노드의 첫번째
//last 노드의 마지막
void ChainPendingNodeList(Node* first, Node* last)
{
//삭제 하는 로직인데 Push 하는것과 동일하게 _pendingList에 삭제할 로직을 뒤에 계속 쌓게 된다
last->next = _pendingList;
while (_pendingList.compare_exchange_weak(last->next, first) == false)
{
}
}
//리스트로 된 스택을 기존 _pendingList에 추가로 넣고 추가된 첫번째 노드를 _pendingList 이 가리키게 한다
void ChainPendingNodeList(Node* node)
{
Node* last = node;
while (last->next)
{
last = last->next;
}
ChainPendingNodeList(node, last);
}
static void DeleteNodes(Node* node)
{
while (node)
{
Node* next = node->next;
delete node;
node = next;
}
}
private :
atomic<Node*> _head; //head 가 다음 노드를 가르키는 방식
atomic<uint32> _popCount = 0; //pop 을 실행중인 스레드 개수
atomic<Node*> _pendingList; //삭제 되어야 할 노드들(첫번째 노드)
};
'운영체제 & 병렬처리 > Multithread' 카테고리의 다른 글
shared_ptr 로 구현한 lock-free 은 lock-free 가 아니다 (3) (0) | 2022.09.22 |
---|---|
Shared_ptr 와 atomic (0) | 2022.09.22 |
LockFree - Stack (1) (0) | 2022.09.15 |
LockBase Queue (0) | 2022.09.14 |
Lock base Stack (0) | 2022.09.14 |