C++ 프로그래밍의 싱글톤 패턴

콘텐츠

1. 스레드 안전성

2. 스레드 불안정성의 예

2.1 예 1:

2.2 예 2:

3. 배고픈 모드와 게으른 모드의 차이점


소위 싱글톤 패턴은 클래스가 메모리에 단 하나의 객체를 갖는다는 것을 의미합니다.

1. 스레드 안전성

소위 스레드 안전성은 다음을 참조합니다. 전체 프로그램에서 여러 스레드가 생성되고 이 코드를 실행하는 각 다중 스레드 프로그램의 결과가 단일 코드와 동일한 경우 이러한 여러 스레드가 동일한 코드 조각을 실행할 수 있습니다. 이것을 실행하는 스레드 프로그램 코드의 결과가 동일하고 변수 값이 예상과 같으면 다중 스레드 프로그램은 스레드로부터 안전합니다.

예시:

main 함수는 두 개의 스레드를 만들고 두 개의 스레드가 Object 클래스의 정적 메서드를 호출하도록 하고 정적 메서드에서 반환된 개체 주소를 인쇄합니다.

#include <iostream>
#include <thread>
using namespace std;
//1、线程安全的情况
class Object
{
private:
	int value;
	static Object instance;//静态变量需要类外初始化
	static int num;
private:
	Object(int x = 0) :value(x) {}//构造函数设为私有,外部函数无法访问
	Object(const Object& obj) = delete;//删除拷贝构造
	Object& operator=(const Object& ob) = delete;//删除等号运算符重载
public:
	static Object& GetInstance()//因为删除了拷贝构造,所以只能以引用或者指针的形式返回这个静态对象本身
	{
		return instance;
	}
};
Object Object::instance(10);
void funa()
{
	Object& obja = Object::GetInstance();//调用静态方法返回静态对象本身
	cout << &obja << endl;
}
void funb()
{
	Object& objb = Object::GetInstance();//调用静态方法返回静态对象本身
	cout << &objb << endl;
}
int main()
{
	thread thra(funa);
	thread thrb(funb);

	thra.join();
	thrb.join();

	return 0;
}

프로그램 실행 결과:

위의 코드가 실행된 후 두 개의 인쇄된 개체 주소는 동일합니다.

설명:

정적 객체 인스턴스는 메인 함수에 들어가기 전에 데이터 영역에 구성되었고 값=10이 설정되었으며 메인 함수에 들어간 후 thra 스레드이든 thrb 스레드이든 이 정적 개체 인스턴스에만 액세스할 수 있습니다. 개체 인스턴스이며 변경 사항이 없으므로 스레드로부터 안전합니다.

2. 스레드 불안정성의 예

다음의 예를 통해 쓰레드 불안정성의 두 가지 경우를 설명하고, 배고픈 모드와 게으른 모드가 무엇인지 설명한다.

2.1 예 1:

배고픈 사람 모드에 대해 설명하기 전에 프로그램 실행 과정에서 정적 변수가 어떻게 초기화되는지 먼저 이해해야 다음 예제를 이해하기에 편리합니다.

프로그램이 낮은 수준에서 리터럴 상수와 변수를 사용하여 정적 변수를 초기화하는 것은 다릅니다.

예시:

void funa(int x)
{
    static int a=10;
}
int main()
{
     funa(10);
}

정적 상수: 메인 함수에 진입하기 전에 static int a=10 을 만나면 데이터 영역에 변수 a가 배치되고 일정 크기의 공간이 열리고 초기 값 10이 할당됩니다. (이것은 스레드로부터 안전합니다).

예시:

void funb(int x)
{
    static int b=x;
}
int main()
{
    funb(10);
}

Static 변수: 메인 함수에 진입하기 전에 static int b=x를 만나십시오; 먼저 데이터 영역에서 이 변수 ​​b에 메모리를 할당하십시오. 얼마나 할당해야 할지 모르기 때문에 먼저 할당하지 마십시오. 이때, 정적 변수 =0은 할당이 없음을 의미하는 플래그 fig가 있습니다. main 함수 뒤에 funb(10)을 호출한 후 할당할 데이터 영역으로 이동하는데 이때 fig 플래그가 0에서 1로 변경되어 초기값이 할당되었음을 나타냅니다. 따라서 변수가 있는 경우 데이터 영역은 먼저 플래그 fig를 수정한 다음 값을 수정해야 합니다(이는 스레드에 안전하지 않습니다. 프로그램에 데이터에서 변수의 플래그 비트 fig를 변경하기 위해 경쟁하는 여러 스레드가 있기 때문입니다. 지역).

프로그램이 정적 변수를 처리하는 방법을 이해한 후 다음 예를 살펴보세요.

main 함수는 각각 Object 클래스의 정적 메서드를 호출하고 매개변수를 x에 전달한 다음 정적 개체를 생성하고 개체를 반환하는 두 개의 스레드를 만듭니다.

//饿汉模式
class Object
{
public:
	int value;
private:
	Object(int x = 0) :value(x) {}//构造函数设为私有,外部函数无法访问
	Object(const Object& obj) = delete;//删除拷贝构造
	Object& operator=(const Object& ob) = delete;//删除等号运算符重载
public:
	static Object& GetInstance(int x)//因为删除了拷贝构造,所以只能以引用或者指针的形式返回这个静态对象本身
	{
		//直接在静态函数中构建这个静态对象并返回其本身
		static Object instance(x);
		return instance;
	}
};
void funa()
{
	Object& obja = Object::GetInstance(10);//调用静态方法返回静态对象本身
	cout << obja.value << endl;
	cout << &obja << endl;
}
void funb()
{
	Object& objb = Object::GetInstance(20);//调用静态方法返回静态对象本身
	cout << objb.value << endl;
	cout << &objb << endl;
}
int main()
{
	thread thra(funa);
	thread thrb(funb);

	thra.join();
	thrb.join();

	return 0;
}

프로그램 실행 결과:

 첫 실행:

두 번째 실행:

위의 예는 스레드로부터 안전하지 않다는 것을 알 수 있습니다.우리는 원래 thra 스레드가 매개변수 10을 x에 전달하고 값=10인 정적 개체를 빌드한 다음 정적 개체를 반환할 것으로 예상했습니다. thrb 스레드는 20의 매개변수를 x에 전달한 다음 값이 20인 정적 개체를 구성하고 정적 개체를 반환합니다. 그러나 프로그램을 실행한 결과는 하나의 개체만 있고 value의 값이 정의되지 않고 우리가 기대한 결과가 아니라는 것입니다.

이유:

static Object instance(x); 이 문장은 메인 함수에 진입하기 전에 데이터 영역에 주소 공간을 할당하고, 메인 함수에 진입한 후 스레드를 시작하며, 두 스레드는 Object 클래스의 정적 메서드를 동시에 실행합니다( 정적 메서드 공유가 하나만 있음) 리소스 경쟁의 문제가 있습니다. 무화과를 0에서 1로 표시할 권리를 놓고 경쟁하는 스레드는 기회를 포착하고 자신의 값을 값에 할당하고 그렇지 않은 스레드는 다시 방문할 때 그랩을 잡으면 fig를 찾을 수 있습니다. 플래그가 1로 설정되어 할당되었음을 나타내므로 정적 메서드만 종료할 수 있습니다.

마지막으로 Hungry Man 모드가 무엇인지 이야기해 보겠습니다. 위의 정적 개체 빌드 예제에서 Hungry Man 모드는 우리가 디자인한 클래스가 로드되기 시작하는 한 다음을 보장하기 위해 싱글톤을 초기화해야 함을 의미한다는 것을 알 수 있습니다. 다중 스레드가 이 고유한 리소스에 액세스한다는 것을 의미합니다. 싱글톤은 이미 존재합니다(위의 예를 들자면: Object 클래스가 로드되기 시작하면 정적 Object instance(x) 객체를 빠르게 초기화하여 다중 스레드가 이것을 호출하도록 해야 합니다. 고유한 정적 메서드인 경우 정적 개체가 이미 존재하고 스레드에서 다른 변경이 필요하지 않음).

2.2 예 2:

예시:

메인 함수는 두 개의 스레드를 시작하고 두 개의 스레드는 각각 Object 클래스의 정적 메서드를 호출합니다. 정적 포인터 pobj가 비어 있는 경우 힙 영역에서 Object 개체를 구성한 다음 정적 반대 포인터 pobj를 반환합니다. 힙 영역에 생성된 Object 객체로 이동한 다음 두 개의 스레드가 각각 포인터가 가리키는 객체의 주소를 인쇄합니다.

class Object
{
public:
	int value;
	static Object* pobj;
private:
	Object(int x = 0) :value(x) {}//构造函数设为私有,外部函数无法访问
	Object(const Object& obj) = delete;//删除拷贝构造
	Object& operator=(const Object& ob) = delete;//删除等号运算符重载
public:
	static Object* GetInstance()//因为删除了拷贝构造,所以只能以引用或者指针的形式返回这个静态对象本身
	{
		if (pobj == NULL)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(10));
			pobj = new Object(10);
		}
		return pobj;
	}
};
Object* Object::pobj = NULL;
void funa()
{
	Object* pa = Object::GetInstance();
	cout << pa << endl;
}
void funb()
{
	Object* pb = Object::GetInstance();
	cout << pb  << endl;
}
int main()
{
	thread thra(funa);
	thread thrb(funb);

	thra.join();
	thrb.join();

	return 0;
}

프로그램 실행 결과:

첫 실행:

두 번째 실행:

프로그램 실행 결과에서 두 스레드가 반환한 개체 주소가 같지 않음을 알 수 있지만 여러 스레드가 스레드 안전 하에서 동일한 프로그램을 실행할 때 반환되는 개체는 하나만 있어야 하고 주소는 고유해야 합니다. . 이것이 지금 일어나는 이유는 다음과 같습니다.

두 스레드 모두 정적 메서드 GetInstance()에 들어갔고 둘 다 pobj 포인터가 비어 있음을 발견하여 둘 다 If 문에 입력한 다음 절전 시간을 설정한 다음 하나를 실행했습니다. pobj = new Object(10); Created a object pobj에 대해 반환하고 개체의 주소를 인쇄하기 위해 반환하면 다른 스레드가 실행 pobj = new Object(10); 다른 개체를 만들고 pobj를 다시 할당한 다음 개체의 주소를 인쇄합니다.

따라서 위의 예에서 볼 수 있습니다. 소위 지연 모드는 우리가 디자인한 클래스가 로드되기 시작할 때 싱글톤을 빠르게 초기화할 필요가 없지만 처음으로 사용될 때까지 기다리는 것을 의미합니다(그래서 게으르다고 한다). 지연 모드의 경우 스레드 안전성을 보장하고 하나의 객체만 생성하려면 객체를 생성하는 코드 부분을 잠글 수 있습니다.

수정된 코드는 다음과 같습니다.
 

#include <mutex>
std::mutex mtx;
class Object
{
public:
	int value;
	static Object* pobj;
private:
	Object(int x = 0) :value(x) {}//构造函数设为私有,外部函数无法访问
	Object(const Object& obj) = delete;//删除拷贝构造
	Object& operator=(const Object& ob) = delete;//删除等号运算符重载
public:
	static Object* GetInstance()//因为删除了拷贝构造,所以只能以引用或者指针的形式返回这个静态对象本身
	{
		std::lock_guard<std::mutex>lock(mtx);
		if (pobj == NULL)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(10));
			pobj = new Object(10);
		}
		return pobj;
	}
};
Object* Object::pobj = NULL;
void funa()
{
	Object* pa = Object::GetInstance();
	cout << pa << endl;
}
void funb()
{
	Object* pb = Object::GetInstance();
	cout << pb  << endl;
}
int main()
{
	thread thra(funa);
	thread thrb(funb);

	thra.join();
	thrb.join();

	return 0;
}

3. 배고픈 모드와 게으른 모드의 차이점

(1) 배고픈 남자 모드:

객체는 클래스가 로드될 때 생성되었으며 프로그램은 호출될 때 이전에 생성된 인스턴스 객체를 직접 얻습니다.

이와 같이 생성된 객체는 정적이기 때문에 항상 데이터 영역의 공간을 차지하게 되지만 호출 시 객체를 생성하는 시간을 절약할 수 있습니다.

(2) 게으른 모드:

개체는 정적 메서드에 액세스할 때만 생성됩니다.

이 방법은 필요할 때만 구성되어 어느 정도 공간을 절약하고 객체가 항상 메모리에서 공간을 낭비하는 것을 방지하지만 사용하면 일정 시간을 소비하고 프로그램을 느리게 만듭니다. .

이 모드는 안전하지 않으며 제어를 위해 잠금이 필요합니다.

Ich denke du magst

Origin blog.csdn.net/m0_54355780/article/details/123188535
Empfohlen
Rangfolge