TSharedPtr<Wraight>의 스레드 안정성있는 삭제 방법
이전 질문들과 겹치는 것을 알고 있으나, 코드 테스트 이후에도 아래와 같은 의문이 풀리지 않아 글을 쓰게 되었습니다.
의문:
전역변수로 지정된 TSharedPtr<RefCountable변수형> "몬스터"가 있습니다. 이를 타 스레드에서 는 복제하여 사용하는 도중, 메인 스레드에서 "몬스터"를 이제 제거하고 싶어 nullptr을 대입합니다. 이때 아래와 같은 문제가 발생합니다.
int32 ReleaseRef()
{
int32 refCount = --_refCount;
// 타 스레드에서 이 타이밍에 "몬스터" 복제하는 문제
if (refCount == 0)
{
delete this;
}
return refCount;
}
실제 테스트:
class Wraight : public RefCountable
{
public:
int testValue = 0;
};
using WraightRef = TSharedPtr<Wraight>;
class Missile : public RefCountable
{
public:
void SetTarget(WraightRef target)
{
_target = target;
// GWraight가 이미 완전히 삭제된 이후 생성된 경우, nullptr 오류 방지
if (!_target.IsNull())
_target->testValue = 5;
}
private:
WraightRef _target;
};
using MissileRef = TSharedPtr<Missile>;
// 스레드들 접근가능한 전역변수
WraightRef GWraight;
int main()
{
// 10번 실험
for (int i = 0; i < 10; i++)
{
// 타겟 소환
GWraight = (new Wraight);
GWraight->ReleaseRef();
// 100'000개의 수많은 미사일 생성 및 타겟 지정
thread t1([]() {
for (int i = 0; i < 100'000; i++)
{
MissileRef missile(new Missile());
missile->ReleaseRef();
missile->SetTarget(GWraight);
}
});
// 타겟 1ms 뒤에 소멸
thread t2([]() {
this_thread::sleep_for(1ms);
GWraight = nullptr;
});
t1.join();
t2.join();
this_thread::sleep_for(3000ms);
}
}해당 코드 실행 이후, 아래와 같은 문제점이 생겼습니다.
케이스A
미사일 발사 후, 제거되는 ~MissileRef()의 ReleaseRef() 내부 delete에서 오류가 발생 합니다.
예상되는 원인:
int32 ReleaseRef()
{
int32 refCount = --_refCount;
if (refCount == 0)
{
// 1. 타 스레드에서 복제
delete this;
// 2. 복제된 객체는 이미 삭제된 _ptr을 들고있음
// 3. 복제에 따라 _refCount = 1
}
return refCount;
}
// 4. 이후에 복제된 객체 삭제되면서 refCount = 0
// 5. 이중 delete 실행 -> 오류
케이스B
타겟인 GWraight가 TSharedPtr<Wraight>(nullptr)를 복사할 때, ReleaseRef() 내부 delete에서 오류가 발생 합니다.
예상되는 원인:
int32 ReleaseRef()
{
int32 refCount = --_refCount;
if (refCount == 0)
{
// 1. 타 스레드에서 복제
// 2. 복제에 따라 _refCount = 1
// 3. 이후에 복제된 객체 삭제되면서 refCount = 0
// 4. delete 실행
delete this; // 5. 이중 delete 실행 -> 오류
}
return refCount;
}
다른 질문에서 refCount가 0이 될 때, 참조 객체가 남아있는 것은 TSharedPtr로 구현되었을 경우 발생하지 않는 문제라고 하셨습니다. 하지만, 어떤 구조로 객체를 삭제해야 위와 같은 문제가 발생하지 않는지 감이 오지 않습니다...
답변 2
1
코드를 봤는데 우선 잘못 접근하는 부분은
// 타겟 1ms 뒤에 소멸
이라는 부분입니다.
shared_ptr로 이미 포인터를 이미 저리 넘긴 상태에서
소멸을 원하는 시점에 딱 터뜨릴 수 없습니다.
(이제 앞으로는 refCount가 0이 되어야 삭제되며,
마지막으로 삭제 막타 치는 객체가 누구인지는 모름)
GWraight 변수에 대한 복사, 및 기타
shared_ptr의 inc/dec 는 atomic하지만,
shared_ptr 변수 자체의 read/write는 현재 보호되지 않으므로
GWraight = nullptr 처리는 문제가 됩니다.
이를 예방하기 위해 표준에서 atomic_shared_ptr이라는 타입이 하나 더 있습니다.
(그게 아니라면 락을 잡고 GWraight = nullptr 등 접근 처리를 하시면 됩니다)
0
복사 및 이동 등에 한해서 thread-safe하다는 것이 어떤 의미지 알 것 같습니다. GWraight = nullptr 같은 shared_ptr 대입에 대해서는 보호받을 수 없다는 것도 이해했습니다.
그렇다면 소멸자의 경우에도 Release() 내부 호출에 따라, 멀티 스레드 환경에서 보호받지 못하는 것으로 봐도 될까요?
1
저 부분은 Release 구현 방법과는 무관하고, 8바이트를 넘어가는 데이터를 동시 접근/수정 하기 때문에 일어나는 일입니다. (애당초 그 순간부터 어떤 일이 일어나도 이상하지 않습니다)
8바이트가 넘어가는 데이터는 한 번에 수정이 되지 않고, 두 번에 걸쳐 write가 적용되는데 그런 반쪽짜리 정보 자체가 존재할 동안엔 원칙상 접근을 금해야 합니다.
대입 제외 나머지 부분을 제대로 동작시켰다면, 멀티쓰레드 환경에서 RefCount 관리로 인해 제대로 보호되어야 정상입니다.
Memory Pool에서 오버플로우 질문입니다.
0
63
2
포토폴리오 및 진로 관련하여 고민입니다.
0
112
1
포토폴리오 관련 고민입니다.
0
62
1
실무에서도 alloc, 스마트포인터 등을 구현해서 쓰는지 궁금합니다.
0
82
2
성능 테스트 결과
0
103
2
게임 서버 Stateful, Stateless 진로 고민
0
121
1
WaitOnAddress와 Sleep의 차이 질문
0
83
1
궁금한거 있습니다.
0
82
2
JobTimer 구동 스레드
0
107
2
TryPop() 동작 관련 질문
0
81
1
로드맵 C#서버 C++서버 방향성 질문
0
148
2
스레드 id를 출력할떄 메인스레드 id도 출력되나요?
0
73
1
생명주기를 위한 의도적 복사
0
86
2
락프리의 실무에서 사용 질문
0
139
2
32bit threadID와 16비트 상위 WriteFlag에 대해
0
101
2
mutex와 sleep 차이점
0
117
1
실무에서는 어떠한 코드 스타일을 사용하는지 궁금합니다
0
152
2
Stomp Allocator의 Release함수에 대한 질문입니다.
0
96
1
공부법 관련해서
0
183
2
MakeShared 함수 관련
0
114
1
지금까지 서버코어에서 만든 내용에 대해 궁금한 점이 있어서 질문 드립니다.
0
144
2
운영체제관련 질문입니다
0
131
1
send하려는 데이터 크기가 크면 memcpy에서 문제가 발생할 것 같습니다.
0
117
2
메모리 풀 질문있습니다.
0
124
1





