Patrón Singleton de programación en C++

contenido

1. Seguridad del hilo

2. Ejemplo de inseguridad de subprocesos

2.1 Ejemplo 1:

2.2 Ejemplo 2:

3. La diferencia entre el modo hambriento y el modo perezoso


El llamado patrón singleton significa que la clase tiene un único objeto en la memoria.

1. Seguridad del hilo

La llamada seguridad de subprocesos se refiere a: cuando se crean varios subprocesos en todo nuestro programa, y ​​estos subprocesos múltiples pueden ejecutar el mismo código, si el resultado de cada programa de subprocesos múltiples que ejecuta este código es el mismo que el de un solo subproceso. programa con subprocesos que ejecuta este El resultado del código es el mismo, y los valores de las variables son los mismos que se esperaba, entonces el programa multiproceso es seguro para subprocesos.

Ejemplo:

La función principal crea dos subprocesos, hace que los dos subprocesos llamen al método estático en la clase Object e imprime la dirección del objeto devuelta por el método estático.

#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;
}

Resultado de la ejecución del programa:

Después de ejecutar el código anterior, las dos direcciones de objeto impresas son las mismas.

explicar:

La instancia del objeto estático se ha construido en el área de datos antes de ingresar a la función principal, y se establece su valor = 10, y luego de ingresar a la función principal, ya sea un subproceso thra o un subproceso thrb, solo puede acceder a este estático instancia del objeto, y no se realizarán cambios en él, por lo que es seguro para subprocesos.

2. Ejemplo de inseguridad de subprocesos

Explique dos casos de inseguridad de subprocesos a través de los siguientes ejemplos y explique qué es el modo hambriento y qué es el modo perezoso.

2.1 Ejemplo 1:

Antes de explicar el modo de hombre hambriento, primero comprendamos cómo se inicializan las variables estáticas durante el proceso de ejecución del programa, lo cual es conveniente para comprender los ejemplos posteriores.

Es diferente que un programa inicialice variables estáticas con constantes literales y variables en el nivel bajo.

Ejemplo:

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

Constante estática: cuando se encuentra static int a=10 antes de ingresar a la función principal, la variable a se colocará en el área de datos, se abrirá un espacio de cierto tamaño y luego se asignará un valor inicial de 10 (esto es seguro para subprocesos).

Ejemplo:

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

Variable estática: encuentre static int b=x antes de ingresar a la función principal; primero asigne memoria a esta variable b en el área de datos. Como no sabe cuánto asignar, no lo haga primero. En este momento, habrá una bandera fig dentro de la variable estática =0 significa que no hay asignación. Llame a funb(10) después de la función principal y luego vaya al área de datos para la asignación. En este momento, el indicador de la figura cambia de 0 a 1, lo que indica que se ha asignado el valor inicial. Entonces, cuando hay una variable, el área de datos es modificar primero el indicador fig y luego modificar el valor (esto no es seguro para subprocesos, porque si el programa tiene varios subprocesos que compiten para cambiar el indicador bit fig de la variable en los datos área).

Después de comprender cómo los programas manejan las variables estáticas, eche un vistazo al siguiente ejemplo:

La función principal crea dos subprocesos, que respectivamente llaman a los métodos estáticos en la clase Object, pasan parámetros a x y luego construyen un objeto estático y devuelven el objeto.

//饿汉模式
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;
}

Resultado de la ejecución del programa:

 Primer intento:

Segunda carrera:

Se puede ver que el ejemplo anterior no es seguro para subprocesos. Originalmente esperábamos que el subproceso thra pasara el parámetro 10 a x, construyera un objeto estático con valor = 10 y luego devolviera el objeto estático. El subproceso thrb pasa un parámetro de 20 a x, luego construye un objeto estático con valor = 20 y devuelve el objeto estático. Pero el resultado de la ejecución del programa es que solo hay un objeto, y el valor de value no está definido, no es el resultado que esperábamos.

razón:

Instancia de objeto estático (x); Esta oración asignará espacio de direcciones en el área de datos antes de ingresar a la función principal, iniciará el subproceso después de ingresar a la función principal y los dos subprocesos ejecutarán el método estático en la clase de Objeto al mismo tiempo ( solo hay un método estático compartir), hay un problema de competencia de recursos, compitiendo por el derecho a marcar fig de 0 a 1, qué subproceso aprovecha la oportunidad, asignará su propio valor a valor, luego el subproceso que no lo hizo agarrarlo encontrará fig cuando vuelva a visitar.La bandera se ha establecido en 1, lo que indica que se ha asignado, por lo que solo puede salir del método estático.

Finalmente, hablemos sobre qué es el modo Hungry Man. Del ejemplo anterior de construir un objeto estático, podemos ver que el modo Hungry Man significa que mientras la clase que diseñamos comience a cargarse, el singleton debe inicializarse para garantizar que múltiples subprocesos acceden a este recurso único. , el singleton ya existe (tome el ejemplo anterior: una vez que la clase de Objeto comienza a cargarse, el objeto de instancia de Objeto estático (x) debe inicializarse rápidamente, para garantizar que múltiples subprocesos llamen a este método estático único cuando, el objeto estático ya existe y el subproceso no requiere otros cambios).

2.2 Ejemplo 2:

Ejemplo:

La función principal inicia dos subprocesos, y los dos subprocesos llaman respectivamente al método estático en la clase Object. Cuando el puntero estático pobj está vacío, se construye un objeto Object a partir del área del montón, y luego se devuelve el puntero estático opuesto pobj. al objeto Object construido en el área del montón, y luego los dos subprocesos imprimen respectivamente la dirección del objeto al que apunta el puntero.

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;
}

Resultado de la ejecución del programa:

Primer intento:

Segunda carrera:

Se puede ver en los resultados de ejecución del programa que las direcciones de objeto devueltas por los dos subprocesos no son las mismas, pero esperamos que cuando varios subprocesos ejecuten el mismo programa bajo la seguridad de subprocesos, solo se devuelva un objeto y la dirección sea única. . La razón por la que esto está sucediendo ahora es:

Ambos subprocesos ingresaron al método estático GetInstance(), y ambos encontraron que el puntero pobj estaba vacío, por lo que ambos ingresaron la declaración If, luego establecí un tiempo de suspensión y luego uno ejecutó pobj = new Object(10); creó un objeto para pobj, y volver a imprimir la dirección del objeto, y luego otro hilo ejecuta pobj = new Object(10); crea otro objeto y reasigna pobj, y luego imprime la dirección del objeto.

Así que se puede ver en el ejemplo anterior: el llamado modo perezoso significa que cuando la clase que diseñamos comienza a cargarse, no necesitamos inicializar el singleton rápidamente, sino esperar hasta que se use por primera vez (para que se dice que es perezoso). Para el modo perezoso, si desea garantizar la seguridad de subprocesos y solo crear un objeto, puede bloquear la parte del código que crea el objeto.

El código modificado es el siguiente:
 

#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. La diferencia entre el modo hambriento y el modo perezoso

(1) Modo hombre hambriento:

El objeto se ha creado cuando se carga la clase, y el programa obtendrá directamente el objeto de instancia construido previamente cuando se llame.

De esta forma, como el objeto construido es estático, siempre ocupará el espacio del área de datos, pero ahorra el tiempo de construcción del objeto al llamar.

(2) Modo perezoso:

Los objetos se construyen solo cuando se accede a métodos estáticos.

Este método solo se construye cuando es necesario usarlo, lo que ahorra espacio hasta cierto punto y evita que los objetos desperdicien espacio en la memoria todo el tiempo, pero cuando se usan, consumirán una cierta cantidad de tiempo y harán que el programa sea más lento. .

Este modo no es seguro y requiere un bloqueo para su control.

Supongo que te gusta

Origin blog.csdn.net/m0_54355780/article/details/123188535
Recomendado
Clasificación