Program Language/C & C++

[C++] 리소스 관리

야곰야곰+책벌레 2022. 1. 13. 15:02
728x90
반응형

함수가 리소스를 획득할 때(즉, 파일을 열고, 여유 저장소에서 일부 메모리를 할당하고, 뮤텍스를 획득하는 등) 리소스가 적절하게 해제되는 시스템의 향후 실행을 위해 종종 필수적이다. 종종 적절한 릴리즈는 호출자에게 반환하기 전에 이를 획득한 함수가 릴리스하도록 할 수 있다.

void use_file(const char∗ fn) // naive code
{
	FILE∗ f = fopen(fn,"r");
	// ... use f ...
	fclose(f);
}

fopen() 호출 후 및 fclose() 호출 전에 문제가 발생하면 예외로 인해 fclose()가 호출되지 않고 use_file()이 종료될 수 있다는 것을 알아차릴 때까지는 그럴듯해 보인다. 예외 처리를 지원하지 않는 언어에서도 똑같은 문제가 발생할 수 있다. 예를 들어 표준 C 라이브러리 함수 longjmp()는 동일한 문제를 일으킬 수 있다. 일반 return문조차도 f를 닫지 않고 use_file을 종료할 수 있다.

use_file() 내결함성을 만들기 위한 첫 번째 시도는 다음과 같다.

void use_file(const char∗ fn) // clumsy code
{
	FILE∗ f = fopen(fn,"r");
	try {
		// ... use f ...
	}
	catch (...) { // catch every possible exception
		fclose(f);
		throw;
	}
	fclose(f);
}

파일을 사용하는 코드는 모든 예외를 포착하고 파일을 닫고 예외를 다시 발생시키는 try-block으로 둘러싸여 있다.

이 코드의 문제는 장황하고 지루하며 잠재적인 비용이 많이 든다는 것이다. 설상가상으로 이러한 코드는 여러 리소스를 확보하고 해제해야 하는 경우 훨씬 더 복잡해진다. 다행히 더 나은 해법이 있다. 문제의 일반적인 형태는 다음과 같다.

void acquire()
{
	// acquire resource 1
	// ...
	// acquire resource n
	// ... use resources ...
	// release resource n
	// ...
	// release resource 1
}

일반적으로 자원은 획득의 역순으로 해제하는 것이 중요하다. 이것은 생성자에 의해 생성되고 소멸자에 의해 소멸되는 로컬 객체의 동작과 매우 유사하다. 따라서 생성자와 소멸자가 있는 클래스의 객체를 사용하여 이러한 리소스 획득 및 해제 문제를 처리할 수 있다. 예를 들어 FILE*처럼 작동하는 File_ptr 클래스를 정의할 수 있다.

class File_ptr {
	FILE∗ p;
public:
	File_ptr(const char∗ n, const char∗ a) // open file n
		: p{fopen(n,a)}
	{
		if (p==nullptr) throw runtime_error{"File_ptr: Can't open file"};
	}
	File_ptr(const string& n, const char∗ a) // open file n
		:File_ptr{n.c_str(),a}
	{ }
	explicit File_ptr(FILE∗ pp) // assume ownership of pp
		:p{pp}
	{
		if (p==nullptr) throw runtime_error("File_ptr: nullptr"};
	}
	// ... suitable move and copy operations ...
	˜File_ptr() { fclose(p); }
	operator FILE∗() { return p; }
};

FILE* 또는 fopen()에 필요한 인수가 주어지면 File_ptr을 구성할 수 있다. 두 경우 모두 File_ptr은 범위가 끝나면 소멸되고 소멸자는 파일을 다는다. File_ptr은 파일을 열 수 없는 경우 예외를 throw한다. 그렇지 않으면 파일 핸들에 대한 모든 작업이 nullptr을 테스트해야 하기 때문이다. 함수는 이제 최소로 축소된다.

void use_file(const char∗ fn)
{
	File_ptr f(fn,"r");
	// ... use f ...
}

소멸자는 함수가 정상적으로 종료되었는지 또는 예외가 발생하여 종료되었는지 여부와 관계없이 호출된다. 즉, 예외 처리 메커니즘을 통해 기본 알고리즘에서 오류 처리 코드를 제거할 수 있다. 결과 코드는 기존 코드보다 더 간단하고 오류가 덜 발생한다.

로컬 객체를 사용하여 리소스를 관리하는 이 기술을 일반적으로 Resource Acquisition Is Initialization(RAII)라고 한다. 이것은 생성자와 소멸자의 속성과 예외처리와의 상호 작용에 의존하는 일반적인 기술이다.

catch(...) 작업에 대해 더 나은 구문을 제공하면 더 나은 솔루션을 제공할 수 있도록 핸들 클래스(RAII 클래스)를 작성하는 것이 성가실 수 있습니다. 그 접근 방식의 문제는 자원이 훈련되지 않은 방식으로 획득되는 곳마다 (대형 프로그램에서 일반적으로 수십 또는 수백 위치) 문제를 잡아서 수정해야 하지만 RAII에서는 한 번만 작성하면 된다.

객체는 생성자가 완료될 때까지 생성된 것으로 간주되지 않는다. 그런 다음 스택 해제가 객체의 소멸자를 호출한다. 하위 객체로 구성된 객체는 해당 하위 객체가 구성된 만큼 구성된다. 배열은 해당 요소가 생성된 만큼 생성된다. (완전히 생성된 요소만 해제하는 동안 소멸됨)

생성자는 객체가 완전하고 올바르게 생성되었는지 확인하려고 합니다. 이것이 불가능할 때 잘 작업된 생성자는 가능한 한 시스템 상태를 생성 전의 상태로 복원한다. 이상적으로, 잘 설계된 생성자는 항상 이러한 대안 중 하나를 달성하고 해당 객체를 반만 구성된 상태로 두지 않는다. 이것은 RAII 기술을 구성원에게 적용하여 간단히 달성할 수 있다.

생성자가 파일 x와 뮤텍스 y의 두 가지 리소스를 획득해야 하는 클래스 X를 고려해보자. 이 획득은 실패하고 예외를 발생시킬 수 있다. 클래스 X의 생성자는 뮤텍스가 아닌 파일 획득을 완료해서는 안된다. 또한 이것은 프로그래머에게 복잡성의 부담을 주지 않고 달성되어야 한다. 획득한 리소스를 나타내기 위해 File_ptr 및 std::unique_lock의 두 클래스 객체를 사용한다. 리소스 획득은 리소스를 나타내는 로컬 객체의 초기화로 표시된다.

class Locked_file_handle {
	File_ptr p;
	unique_lock<mutex> lck;
public:
	X(const char∗ file, mutex& m)
		: p{file ,"rw"}, // acquire ‘‘file’’
		lck{m} // acquire ‘‘m’’
	{}
	// ...
};

이제 로컬 객체의 경우와 마찬가지로 구현이 모든 bookkeeping을 처리한다. 사용자가 추적할 필요가 전혀 없다. 예를 들어, p가 생성된 후 lck가 생성되기 전에 예외가 발생하면 p에 대한 소멸자가 호출되지만 lck에 대해서는 호출되지 않는다.

이것은 리소스 획득을 위한 이 간단한 모델이 준수되는 경우 생성자의 작성자가 명시적인 예외 코드를 작성할 필요가 없음을 의미한다.

가장 일반적인 리소스는 메모리이며, 무자열, 벡터 및 기타 표준 컨테이너는 RAII를 사용하여 획득 및 릴리스를 암시적으로 관리한다. 새로 만들기(및 삭제도 가능)를 사용하는 임시 메모리 관리와 비교할 때 이것은 많은 작업을 절약하고 많은 오류를 방지한다.

로컬 객체가 아닌 객체에 대한 포인터가 필요한 경우 표준 라이브러리 유형 unique_ptr 및 shared_ptr을 사용하여 leak을 방지할 수 있다.

728x90
반응형

'Program Language > C & C++' 카테고리의 다른 글

[C++] throwing exception  (0) 2022.01.13
[C++] Finally  (0) 2022.01.13
[C++] 미리 정의된 매크로  (0) 2022.01.13
[C++] constexpr 함수  (0) 2022.01.13
[C++] 함수의 지정자와 수정자  (0) 2022.01.13