본문 바로가기

STUDY/C++

스마트포인터 shared_ptr 구현하기 (2) - thread safety

thread safety ?


c++ refenence 를 보면 다음과 같이 나와 있습니다. 

더보기

All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object.

해석
모든 멤버 함수(복사 생성자, 복사 할당 연산자를 포함한)는 해당 인스턴스들이 복사되거나 똑같은 객체 소유권을 공유한다해도 다른 공유 포인터의 인스턴스에서 추가적인 동기화 없이 멀티 스레드에 의해 호출될 수 있다. 


 

다음 예제를 실행 해보겠습니다. 

 

#include "shared_ptr.h"
#include "shared_ptr.hpp"

using namespace std;

shared_ptr<int> global_ptr;
void thread1()
{
	while (true)
	{
		shared_ptr<int> temp;
		temp = global_ptr;
		if (1 != (*temp)) cout << "Error \n";
	}
}

int main(void)
{
	global_ptr = shared_ptr<int>(new int(1));
	thread th(thread1);

	while (true)
	{
		shared_ptr<int> _newP = shared_ptr<int>(new int(1));
		global_ptr = _newP;
	}

	system("pause");
	return 0;
}

 

 

 

왜 이런 현상이 발생하는지 알아보면,

thread1 에서 

shared_ptr<int> temp; 
temp = global_ptr;


바로 이구간, temp에 글로벌 shared_ptr을 넣는 부분에서 컨텍스트 스위칭이 일어나며 mainThread로 옮겨져 

eb_shared_ptr<int> _newP = eb_shared_ptr<int>(new int(1)); 
global_ptr = _newP;



이 코드가 실행되었다면, thread1에 가지고있는 temp는 값이 사라지게 됩니다. 
따라서 Error텍스트가 노출되고, 스코프를 벗어나며 해제된 메모리에 접근하여 _refCount를 가져와서 크래시까지 발생하게 되는 것입니다. 

 


이 방법을 해결하기 위해서는 두가지 방법이 있습니다. 


1. atomic_load를 사용한다. 

더보기

atomic_load를 사용하면 깔끔한 구현이 만들어집니다.  

shared_ptr의 생성과 소멸이 잦은 경우가 아니라면 성능에 큰 지장이 있지도 않을 것 같습니다. 
atomic_load를 살펴보면 내부에 전역 spin lock이 구현되어 있습니다. 
atomic_load을 사용하는 객체는 타입이 다른 shraed_ptr인 경우에도 모두 단 한개의 lock만을 이용하게 될 것입니다. 생성과 소멸이 매우 잦은 경우라면 문제가 생길수도 있다고 생각합니다. 


단점이 있다면 atomic_load를 강제할 수 있도록 만들어 놓지 않는다면 빼먹고 그냥 make_shared를 사용하여 로드를 할 수도 있겠죠.. 

 

 

2. c++20에 있는 atomic_shared_ptr을 사용한다. 

더보기

c++20에서는 atomic_shared_ptr을 사용할 수 있습니다.
이 atomic_shared_ptr은 LockFree로 구현되어 있어 성능상 매우 좋을것으로 생각됩니다.