해결된 질문
작성
·
119
0
이전 질문들과 겹치는 것을 알고 있으나, 코드 테스트 이후에도 아래와 같은 의문이 풀리지 않아 글을 쓰게 되었습니다.
의문:
전역변수로 지정된 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 등 접근 처리를 하시면 됩니다)
1
복사 및 이동 등에 한해서 thread-safe하다는 것이 어떤 의미지 알 것 같습니다. GWraight = nullptr 같은 shared_ptr 대입에 대해서는 보호받을 수 없다는 것도 이해했습니다.
그렇다면 소멸자의 경우에도 Release() 내부 호출에 따라, 멀티 스레드 환경에서 보호받지 못하는 것으로 봐도 될까요?