인프런 커뮤니티 질문&답변

맞수님의 프로필 이미지
맞수

작성한 질문수

[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버

Reference Counting

TSharedPtr<Wraight>의 스레드 안정성있는 삭제 방법

해결된 질문

작성

·

137

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

케이스1A.PNG

미사일 발사 후, 제거되는 ~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

케이스2A.PNG

타겟인 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

Rookiss님의 프로필 이미지
Rookiss
지식공유자

코드를 봤는데 우선 잘못 접근하는 부분은

// 타겟 1ms 뒤에 소멸
이라는 부분입니다.
shared_ptr로 이미 포인터를 이미 저리 넘긴 상태에서
소멸을 원하는 시점에 딱 터뜨릴 수 없습니다.
(이제 앞으로는 refCount가 0이 되어야 삭제되며,
마지막으로 삭제 막타 치는 객체가 누구인지는 모름)

GWraight 변수에 대한 복사, 및 기타
shared_ptr의 inc/dec 는 atomic하지만,
shared_ptr 변수 자체의 read/write는 현재 보호되지 않으므로
GWraight = nullptr 처리는 문제가 됩니다.

이를 예방하기 위해 표준에서 atomic_shared_ptr이라는 타입이 하나 더 있습니다.
(그게 아니라면 락을 잡고 GWraight = nullptr 등 접근 처리를 하시면 됩니다)

맞수님의 프로필 이미지
맞수
질문자

복사 및 이동 등에 한해서 thread-safe하다는 것이 어떤 의미지 알 것 같습니다. GWraight = nullptr 같은 shared_ptr 대입에 대해서는 보호받을 수 없다는 것도 이해했습니다.

그렇다면 소멸자의 경우에도 Release() 내부 호출에 따라, 멀티 스레드 환경에서 보호받지 못하는 것으로 봐도 될까요?

Rookiss님의 프로필 이미지
Rookiss
지식공유자

저 부분은 Release 구현 방법과는 무관하고, 8바이트를 넘어가는 데이터를 동시 접근/수정 하기 때문에 일어나는 일입니다. (애당초 그 순간부터 어떤 일이 일어나도 이상하지 않습니다)

8바이트가 넘어가는 데이터는 한 번에 수정이 되지 않고, 두 번에 걸쳐 write가 적용되는데 그런 반쪽짜리 정보 자체가 존재할 동안엔 원칙상 접근을 금해야 합니다.

대입 제외 나머지 부분을 제대로 동작시켰다면, 멀티쓰레드 환경에서 RefCount 관리로 인해 제대로 보호되어야 정상입니다.

맞수님의 프로필 이미지
맞수
질문자

늦은 시간까지 감사합니다.

1

Rookiss님의 프로필 이미지
Rookiss
지식공유자

전체 코드를 압축해서 rookiss@naver.com 로 보내주시기 바랍니다.

맞수님의 프로필 이미지
맞수

작성한 질문수

질문하기