El uso de std::thread

Introducción a la programación multiproceso

Cuando se trata de programación multiproceso, debe mencionar el paralelismo y la concurrencia.El multiproceso es un medio para lograr la concurrencia y el paralelismo.

  • Paralelismo significa que dos o más operaciones independientes se realizan simultáneamente .

  • La concurrencia se refiere a la ejecución de múltiples operaciones dentro de un período de tiempo .

En la era de un solo núcleo, múltiples subprocesos son concurrentes y se ejecutan a su vez dentro de un período de tiempo; en la era de múltiples núcleos, múltiples subprocesos pueden lograr un verdadero paralelismo y una ejecución paralela verdaderamente independiente en múltiples núcleos. Por ejemplo, los 4 núcleos y 4 subprocesos comunes pueden funcionar en paralelo con 4 subprocesos; 4 núcleos y 8 subprocesos utilizan la tecnología de hiperprocesamiento, que simula un núcleo físico como 2 núcleos lógicos y pueden funcionar en paralelo con 8 subprocesos.

Enfoques de la programación concurrente

En general, hay dos formas de lograr la concurrencia: concurrencia de procesos múltiples y concurrencia de subprocesos múltiples.

Concurrencia multiproceso

El uso de la simultaneidad de múltiples procesos consiste en dividir una aplicación en múltiples procesos independientes (cada proceso tiene solo un subproceso), y estos procesos independientes pueden comunicarse entre sí para completar tareas juntos.

Dado que el sistema operativo proporciona una gran cantidad de mecanismos de protección para los procesos para evitar que un proceso modifique los datos de otro proceso, es más fácil escribir código seguro utilizando múltiples procesos que múltiples subprocesos. Pero esto también crea dos desventajas de la concurrencia multiproceso:

  • La comunicación entre procesos, ya sea mediante señales, sockets, archivos, conductos, etc., es más complicada de usar, o más lenta, o ambas cosas.
  • Ejecutar múltiples procesos es costoso y el sistema operativo asigna una gran cantidad de recursos para administrar estos procesos.

Cuando múltiples procesos completan la misma tarea al mismo tiempo, es inevitable operar los mismos datos y comunicarse entre sí entre procesos.Las dos deficiencias anteriores también determinan que la concurrencia de múltiples procesos no sea una buena opción.

concurrencia de subprocesos múltiples

La concurrencia de subprocesos múltiples se refiere a la ejecución de múltiples subprocesos en el mismo proceso.

Aquellos que tienen conocimiento sobre el sistema operativo deben saber que los hilos son procesos livianos, y cada hilo puede ejecutar de forma independiente diferentes secuencias de instrucciones, pero los hilos no poseen recursos de forma independiente, sino que dependen del proceso que los creó. Es decir, múltiples subprocesos en el mismo proceso comparten el mismo espacio de direcciones, pueden acceder a la mayoría de los datos en el proceso y se pueden pasar punteros y referencias entre subprocesos . De esta manera, varios subprocesos en el mismo proceso pueden compartir datos y comunicarse fácilmente, lo que es más adecuado para operaciones simultáneas que para procesos.

Debido a la falta de mecanismos de protección proporcionados por el sistema operativo, cuando los subprocesos múltiples comparten datos y se comunican, los programadores deben trabajar más para garantizar que las operaciones en los segmentos de datos compartidos se realicen en el orden esperado de operaciones y tratar de evitar * *punto muerto**.

std::threadIntroducción

Antes de C++ 11, las plataformas Windows y Linux tenían sus propios estándares de subprocesos múltiples, y los subprocesos múltiples escritos en C++ a menudo dependían de plataformas específicas.

  • La plataforma de Windows proporciona la API win32 para la creación y gestión de subprocesos múltiples;
  • Bajo Linux, existe el estándar de subprocesos múltiples POSIX, y la API proporcionada por la biblioteca Threads o Pthreads puede ejecutarse en un sistema similar a Unix;

En el nuevo estándar C ++ 11, los subprocesos múltiples se pueden administrar simplemente utilizando la biblioteca de subprocesos. La biblioteca de subprocesos se puede considerar como una capa de empaque para la API de subprocesos múltiples de diferentes plataformas; por lo tanto, los programas escritos con la biblioteca de subprocesos proporcionada por el nuevo estándar son multiplataforma.

Una implementación multiproceso simple

La biblioteca estándar de C++ 11 proporciona una biblioteca de subprocesos múltiples, que requiere un #include <thread>archivo de encabezado cuando se usa. El archivo de encabezado incluye principalmente clases de administración de subprocesos std::thready otras clases relacionadas con la administración de subprocesos. Aquí hay un ejemplo simple que usa la biblioteca de subprocesos múltiples de C ++:

#include <iostream>
#include <thread>

using namespace std;

void output(int i)
{
	cout << i << endl;
}

int main()
{
	
	for (uint8_t i = 0; i < 4; i++)
	{
		thread t(output, i);
		t.detach();	
	}
		
	getchar();
	return 0;
}

En un ciclo for, cree 4 subprocesos para generar los números 0, 1, 2, 3 respectivamente, y genere una nueva línea al final de cada número. La sentencia thread t(output, i)crea un subproceso t, que se ejecuta output, y el segundo parámetro i es outputel parámetro pasado a Se inicia automáticamente después de que se completa la creación, t.detachlo que indica que el subproceso está permitido en segundo plano y no es necesario esperar a que se complete el subproceso y continuar ejecutando las siguientes declaraciones. La función de este código es muy sencilla, si se ejecuta secuencialmente el resultado es fácil de predecir:

0
1
2
3

Sin embargo, bajo subprocesos múltiples paralelos, los resultados de su ejecución son varios, por ejemplo, el siguiente es el resultado de ejecutar el código una vez:

01

2
3

Esto implica el problema central de la programación multiproceso: competencia de recursos .

Suponiendo que la CPU tiene 4 núcleos, se pueden ejecutar 4 subprocesos al mismo tiempo, pero solo hay una consola, y solo un subproceso puede poseer esta consola única al mismo tiempo y generar los números. Numere los cuatro subprocesos creados por el código anterior: t0, t1, t2, t3, y genere los números respectivamente: 0, 1, 2, 3. Con referencia a los resultados de ejecución en la figura anterior, la propiedad de la consola se transfiere de la siguiente manera:

  • t0 posee la consola y emite el número 0, pero no viene y genera un carácter de nueva línea, pero la propiedad del control se transfiere a t1; (0)
  • t1 completa su propia salida, el subproceso t1 completa (1\n)
  • La propiedad de la consola se transfiere a t0 y se genera un carácter de nueva línea (\n).
  • t2 posee la consola, salida completa (2\n)
  • t3 posee la consola, salida completa (3\n)

Dado que la consola es un recurso del sistema, la administración de la propiedad de la consola aquí la realiza el sistema operativo. Sin embargo, si varios subprocesos comparten los datos en el espacio de proceso, debe escribir su propio código para controlar cuándo cada subproceso puede poseer los datos compartidos para la operación.

La gestión de datos compartidos y la comunicación entre hilos son los dos núcleos de la programación multihilo.

gestión de hilos

Cada aplicación tiene al menos un proceso y cada proceso tiene al menos un subproceso principal Además del subproceso principal, se pueden crear múltiples subprocesos en un proceso. Cada subproceso necesita una función de entrada. Si la función de entrada regresa y sale, el subproceso también saldrá. El subproceso principal es el mainsubproceso con la función como función de entrada.

En la biblioteca de subprocesos de C++ 11, la gestión de subprocesos se coloca en la clase std::thread, y std::threadse puede crear e iniciar un subproceso, y se puede suspender y finalizar el subproceso.

iniciar un hilo

La biblioteca de subprocesos de C++ 11 es muy simple para iniciar un subproceso. Solo necesita crear un std::threadobjeto, se iniciará un subproceso y el std::threadobjeto se utilizará para administrar el subproceso.

do_task();
std::thread(do_task);

La función pasada se crea aquí std::threadDe hecho, su constructor necesita un tipo invocable (invocable), siempre que tenga una instancia del tipo de llamada de función. Entonces, en lugar de pasar la función, puede usar:

  • expresión lambda

    Use la expresión lambda para iniciar el hilo para generar números

    for (int i = 0; i < 4; i++)
    {
    	thread t([i]{
    		cout << i << endl;
    	});
    	t.detach();
    }
  • Instancias de clases que sobrecargan el operador ()

    Salida digital de subprocesos múltiples usando una clase que sobrecarga el operador ()

    class Task
    {
    public:
    	void operator()(int i)
    	{
    		cout << i << endl;
    	}
    };
    
    int main()
    {
    	
    	for (uint8_t i = 0; i < 4; i++)
    	{
    		Task task;
    		thread t(task, i);
    		t.detach();	
    	}
    }

Al pasar el objeto de función al std::threadconstructor, tenga en cuenta un error de análisis de sintaxis de C++ (el análisis más desconcertante de C++). Si se pasa una variable temporal al std::threadconstructor en lugar de una variable con nombre, se producirá un error de análisis de sintaxis. El siguiente código:

std::thread t(Task());

Esto es equivalente a declarar una función t cuyo tipo de retorno es thread, en lugar de iniciar un nuevo hilo. Esto se puede evitar con la nueva sintaxis de inicialización

std::thread t{Task()};

Después de que se inicia el subproceso, es necesario determinar cómo esperar el final de la ejecución del subproceso antes de la destrucción asociada con el subprocesothread .

C ++ 11 tiene dos formas de esperar a que finalice un hilo:

  • En el modo de separación , el subproceso iniciado se ejecuta de forma independiente en segundo plano y el código actual continúa ejecutándose sin esperar el final del nuevo subproceso. Este es el enfoque utilizado en el código anterior.
    • Llamar a detach significa que el objeto hilo está completamente separado del hilo que representa;
    • El subproceso después de la separación no está restringido ni controlado, y se ejecutará por separado hasta que el recurso se libere después de la ejecución, lo que puede considerarse como un subproceso daemon;
    • Después de la separación, el objeto hilo ya no representa ningún hilo;
    • joinable() == false después de la separación, incluso si aún se está ejecutando;
  • El método de unión espera a que se complete el subproceso iniciado antes de continuar con la ejecución. Si el código anterior usa este método, su salida será 0, 1, 2, 3, porque cada vez que se complete la salida del hilo anterior, se realizará el siguiente ciclo y se iniciará el siguiente hilo nuevo.
    • Solo los subprocesos activos pueden llamar a unirse, lo que puede verificarse mediante la función joinable();
    • joinable() == true significa que el subproceso actual es un subproceso activo antes de llamar a la función de unión;
    • Los objetos creados por el constructor predeterminado se pueden unir() == false;
    • join solo se puede llamar una vez, después de lo cual joinable se volverá falso, lo que indica que la ejecución del subproceso está completa;
    • El subproceso que llama a ternimate() debe poder unirse() == false;
    • Si el subproceso no llama a la función join(), incluso si se ejecuta, sigue siendo un subproceso activo, es decir, joinable() == true, y aún se puede llamar a la función join();

En cualquier caso, asegúrese de llamar o threadantes de destruir para determinar cómo se ejecuta el subproceso.t.joint.detach

Cuando se usa el método de unión, el código actual se bloqueará y la ejecución continuará hasta que el hilo termine de salir;

Sin embargo, el uso del método de separación no afectará el código actual. El código actual continúa ejecutándose hacia abajo, y el nuevo hilo creado se ejecuta simultáneamente. En este momento, se debe prestar especial atención: el uso del nuevo hilo creado para variables del ámbito actual , y la creación de un nuevo subproceso Después del final del ámbito del subproceso, es posible que el subproceso aún se esté ejecutando.En este momento, las variables locales se han destruido con la finalización del ámbito. Si el subproceso sigue utilizando la referencia o el puntero de la variable local , se producirá un error inesperado, y este error es muy grave. Difícil de solucionar. Por ejemplo:

auto fn = [](const int *a)
{
    for (int i = 0; i < 10; i++)
    {
        cout << *a << endl;
    }
};

[fn]
{
    int a = 1010;
    thread t(fn, &a);
    t.detach();
}();

En la expresión lambda, use fn para iniciar un nuevo subproceso, use el puntero de la variable local a en el nuevo subproceso y configure el modo de ejecución del subproceso para que se separe. De esta manera, después de que finaliza la ejecución de la expresión lamb, la variable a se destruye, pero el subproceso que se ejecuta en segundo plano todavía usa el puntero de la variable destruida a, lo que puede conducir a resultados incorrectos.

Por lo tanto, al ejecutar un subproceso en modo separado, copie los datos locales a los que accede el subproceso en el espacio del subproceso (pasar por valor) y asegúrese de que el subproceso no utilice referencias o punteros a variables locales, a menos que esté seguro de que el subproceso el subproceso estará en el local La ejecución finaliza antes de que finalice el ámbito.

Por supuesto, este problema no ocurrirá si usa el método de unión, completará la salida antes del final del alcance.

Esperando que el hilo se complete en condiciones excepcionales

Cuando se decide dejar que el subproceso se ejecute en segundo plano en modo separado, se puede threadllamar inmediatamente después de la instancia creada detach, de modo que el subproceso se separará de la instancia subsiguiente, incluso si se destruye threadla instancia anómala , el subproceso aún puede threadestar garantizado para ejecutarse en segundo plano.

Pero cuando el subproceso se ejecuta en el modo de unión, debe llamar joinal método en la posición adecuada del subproceso principal. Si joinse produce una excepción antes de la llamada, threadse destruye y el subproceso terminará con la excepción. Para evitar que el subproceso finalice por excepción, o por alguna razón, como que el subproceso acceda a variables locales, es necesario asegurarse de que el subproceso debe completarse antes de que la función finalice, y es necesario asegurarse de que la función se llama antes de que la función salga.join

void func() {
	thread t([]{
		cout << "hello C++ 11" << endl;
	});

	try
	{
		do_something_else();
	}
	catch (...)
	{
		t.join();
		throw;
	}
	t.join();
}

El código anterior puede garantizar que el método se llamará en condiciones normales o anormales join, de modo que el subproceso definitivamente se funccompletará antes de que finalice la función. Pero al usar este método, no solo el código es largo, sino que también habrá algunos problemas de alcance, lo que no es una buena solución.

Un mejor método es Resource Acquisition Is Initialization (RAII, Resource Acquisition Is Initialization), que proporciona una clase y la llama en el destructor join.

class thread_guard
{
	thread &t;
public :
	explicit thread_guard(thread& _t) :
		t(_t){}

	~thread_guard()
	{
		if (t.joinable())
			t.join();
	}

	thread_guard(const thread_guard&) = delete;
	thread_guard& operator=(const thread_guard&) = delete;
};

void func(){

	thread t([]{
		cout << "Hello thread" <<endl ;
	});

	thread_guard g(t);
}

En cualquier caso, cuando la función sale, la variable local gllama a su destructor para destruir, por lo que joinse puede garantizar que se llame.

Pasar parámetros al hilo

También es muy simple pasar parámetros a la función llamada por el hilo, solo necesita threadpasarlos en orden al construir la instancia. Por ejemplo:

void func(int *a,int n){}

int buffer[10];
thread t(func,buffer,10);
t.join();

Cabe señalar que, de forma predeterminada, los parámetros pasados ​​​​se copiarán en el espacio del subproceso mediante la copia, incluso si el tipo del parámetro es una referencia. Por ejemplo:

void func(int a,const string& str);
thread t(func,3,"hello");

funcEl segundo parámetro de es string &, y se pasa un literal de cadena. Después de que el literal const char*se pase al espacio del subproceso como un tipo, se convertirá a ** en el espacio del subproceso string.

Si usa referencias para actualizar objetos en hilos, debe prestar atención. El valor predeterminado es copiar el objeto en el espacio del subproceso, que se refiere al objeto en el espacio del subproceso copiado, no al objeto que originalmente deseaba cambiar. como sigue:

class _tagNode
{
public:
	int a;
	int b;
};

void func(_tagNode &node)
{
	node.a = 10;
	node.b = 20;
}

void f()
{
	_tagNode node;

	thread t(func, node);
	t.join();

	cout << node.a << endl ;
	cout << node.b << endl ;
}

En el subproceso, los campos a y b del objeto se establecen en nuevos valores, pero una vez que finaliza la llamada del subproceso, los valores de estos dos campos no cambiarán. Esto se debe a que la referencia es en realidad nodeuna copia de la variable local, no nodeen sí misma. Al pasar el objeto al hilo, llame a std::ref, nodepase la referencia al hilo, no una copia. Por ejemplo:thread t(func,std::ref(node));

También puede usar funciones de miembros de clase como funciones de subprocesos, los ejemplos son los siguientes

class _tagNode{

public:
	void do_some_work(int a);
};
_tagNode node;

thread t(&_tagNode::do_some_work, &node,20);

El subproceso creado anteriormente se llamará node.do_some_work(20), el tercer parámetro es el primer parámetro de la función miembro, y así sucesivamente.

transferir la propiedad del hilo

threadEs movible (movible), pero no copiable (copyable). Puede movecambiar la propiedad del subproceso para decidir con flexibilidad cuándo se une o se separa el subproceso.

thread t1(f1);
thread t3(move(t1));

Transfiera el subproceso de t1 a t3. En este momento, t1 ya no tiene la propiedad del subproceso y puede ocurrir una excepción en la llamada t1.join. t1.detachDebe usar t3 para administrar el subproceso. Esto también significa que threadpuede usarse como el tipo de devolución de la función, o pasarse a la función como un parámetro, lo que puede administrar los subprocesos de manera más conveniente.

El tipo de ID del hilo es std::thread::id, y hay dos formas de obtener el ID del hilo.

  • Consíguelo directamente llamando threada la instanciaget_id()
  • this_thread::get_id()Llamar Obtener en el hilo actual

 

 Autor del artículo:  Immortalqx

 Enlace del artículo:  http://Immortalqx.github.io/2021/12/04/cpp-notes-3/

 Declaración de derechos de autor:  a menos que se indique lo contrario, todos los artículos de este blog adoptan  el acuerdo de licencia CC BY 4.0  . ¡Indique la fuente  Immortalqx al reimprimir  !

Supongo que te gusta

Origin blog.csdn.net/weixin_58045467/article/details/131003378
Recomendado
Clasificación