Interacción de datos C ++ entre múltiples subprocesos

Consulte el blog https://blog.csdn.net/hai008007/article/details/80246437 para organizarlo y modificarlo.

La interacción de datos entre varios subprocesos en el mismo proceso es inevitable. Las colas y los datos compartidos son formas comunes de lograr la interacción de datos entre varios subprocesos. La cola encapsulada es relativamente menos propensa a errores y los datos compartidos son los más básicos y propensos a errores. , porque causará contención de datos, es decir, más de un hilo intenta tomar un recurso al mismo tiempo, como leer y escribir un bloque de memoria, como se muestra en el siguiente ejemplo:

#include <iostream>
#include <thread>
 
using namespace std;
 
#define COUNT 10000
 
void inc(int *p){
    
    
    for(int i = 0; i < COUNT; i++){
    
    
        (*p)++;
    }
}
 
int main()
{
    
    
    int a = 0;
    
    thread ta(inc, &a);
    thread tb(inc, &a);
    
    ta.join();
    tb.join();
    
    cout << " a = " << a << endl;
    return 0;
}

El ejemplo anterior es un simple intercambio de datos, se puede ver que dos hilos leen y escriben la dirección de memoria de & a al mismo tiempo. En la superficie, después de que se ejecutan los dos subprocesos, el valor final a debería ser COUNT * 2, pero de hecho no es el caso, porque puede haber dos subprocesos que necesiten acceder a esta pieza de memoria al mismo tiempo para la operación. , y el hilo se interrumpirá. Para resolver este problema, para tipos básicos simples de datos como caracteres, números enteros, punteros, etc., C ++ proporciona la clase de plantilla atómica atómica, y para objetos complejos, proporciona el mecanismo de bloqueo más utilizado, como la clase de exclusión mutua mutex. , cerradura de puerta lock_guard, cerradura única unique_lock, condición variable condition_variable, etc.

std :: atómico

Para los subprocesos, los tipos atómicos pertenecen a datos de "tipo de recurso", lo que significa que varios subprocesos normalmente solo pueden acceder a una copia de un solo tipo atómico.
Por lo tanto, en C ++ 11, los tipos atómicos solo se pueden construir a partir de sus tipos de parámetros de plantilla.
El estándar no permite que los tipos atómicos realicen operaciones como la construcción de copias, la construcción de movimientos y el uso de operator =. De hecho, las operaciones como la construcción de copias, la construcción de movimientos y las plantillas operator = de clases atómicas se eliminan de forma predeterminada.
Sin embargo, es posible construir variables del tipo de parámetro de plantilla T a partir de las variables de tipo atómico. Esto se debe a que la plantilla de clase atómica siempre define la función de conversión de tipo de atomic <T> a T.Cuando sea necesario, el compilador implícitamente Complete la conversión del tipo atómico al tipo de parámetro de plantilla correspondiente.
El archivo de encabezado C11 <cstdatomic> simplemente define el tipo atómico correspondiente al tipo incorporado

atómico Tipos de
atomic_bool bool
atomic_char carbonizarse
atomic_schar char firmado
atomic_uchar char sin firmar
atomic_int En t
atomic_uint int sin firmar
atomic_short corto
atomic_ushort corto sin firmar
atomic_long largo
atomic_ulong largo sin firmar
atomic_llong largo largo
atomic_ullong sin firmar mucho tiempo
atomic_char16_t char16_t
atomic_char32_t char32_t
atomic_wchar_t wchar_t

Nuevamente, por ejemplo:

#include <iostream>
#include <thread>
#include <atomic>
 
using namespace std;
 
#define COUNT 10000
 
void inc(atomic<int> *p){
    
    
    for(int i = 0; i < COUNT; i++){
    
    
        (*p)++;
    }
}
 
int main()
{
    
    
    atomic<int> a{
    
    0};
    
    thread ta(inc, &a);
    thread tb(inc, &a);
    
    ta.join();
    tb.join();
    
    cout << " a = " << a << endl;
    return 0;
}

std :: lock_guard

Tomemos primero un pequeño ejemplo:

mutex m;
m.lock();
sharedVariable= getVar();
m.unlock();

En este código, el mutex m asegura que el acceso de la parte clave sharedVariable = getVar (); es secuencial.
Medios secuenciales: en este caso especial, cada hilo obtiene acceso a la parte clave en orden.
El código es simple, pero es propenso a bloquearse. Si la parte crítica arroja una excepción o el programador simplemente se olvida de desbloquear el mutex, se producirá un punto muerto.

Usando std :: lock_guard, podemos hacerlo de manera más elegante:

{
    
    
  std::mutex m,
  std::lock_guard<std::mutex> lockGuard(m);
  sharedVariable= getVar();
}

Es fácil. Pero, ¿qué son el corchete de apertura {y el corchete de cierre}?
Para garantizar que el ciclo de vida de std :: lock_guard solo sea válido en este {}.
En otras palabras, cuando el ciclo de vida abandona la zona crítica, su ciclo de vida finaliza.
Para ser precisos, en ese momento, se llamó al destructor de std :: lock_guard, y sí, se liberó el mutex. El proceso es completamente automático Además, si getVar () lanza una excepción cuando sharedVariable = getVar () también libera el mutex. Por supuesto, el alcance del cuerpo o bucle de la función también limita el ciclo de vida del objeto.

#include <iostream>
#include <thread>
#include <mutex>
 
using namespace std;
 
#define COUNT 10000

static mutex g_mutex;
 
void inc(int *p){
    
    
    for(int i = 0; i < COUNT; i++){
    
    
    	lock_guard<mutex> lck(g_mutex);
        (*p)++;
    }
}
 
int main()
{
    
    
    int a{
    
    0};
    
    thread ta(inc, &a);
    thread tb(inc, &a);
    
    ta.join();
    tb.join();
    
    cout << " a = " << a << endl;
    return 0;
}

Además, también se puede utilizar unique_lock ().
Unique_lock es una plantilla de clase. En el trabajo, generalmente lock_guard (recomendado); lock_guard reemplaza lock () y unlock () de mutex; unique_lock es mucho más flexible que lock_guard, menos eficiente y ocupa un poco más de memoria. El uso específico de unique_lock () se explica por separado, por lo que no lo repetiré aquí.

std :: mutex

std :: mutex es el mutex más básico en C ++ 11. El objeto std :: mutex proporciona la característica de propiedad exclusiva, es decir, no admite el bloqueo recursivo de objetos std :: mutex, mientras que std :: recursive_lock El mutex El objeto se puede bloquear de forma recursiva.

#include <iostream>
#include <thread>
#include <mutex>
 
using namespace std;
 
#define COUNT 10000

static mutex g_mutex;
 
void inc(int *p){
    
    
    for(int i = 0; i < COUNT; i++){
    
    
    	g_mutex.lock();
        (*p)++;
        g_mutex.unlock();
    }
}
 
int main()
{
    
    
    int a{
    
    0};
    
    thread ta(inc, &a);
    thread tb(inc, &a);
    
    ta.join();
    tb.join();
    
    cout << " a = " << a << endl;
    return 0;
}

std :: condition_variable

Para notificaciones de eventos entre subprocesos, C ++ 11 proporciona una variable de condición class condition_variable (que puede considerarse como una encapsulación de pthread_cond_t). El uso de variables de condición permite que un subproceso espere notificaciones de otros subprocesos (esperar, esperar_para, esperar_hasta). , u otro El hilo envía notificaciones (notificar_uno, notificar_todos). La variable de condición debe usarse junto con el candado. En espera, debido a que hay desbloqueo y rebloqueo, debe usar un candado que se pueda desbloquear y bloquear manualmente mientras espera , como unique_lock, pero no se puede usar lock_guard, un ejemplo es el siguiente:

#include <thread>
#include <iostream>
#include <condition_variable>

# define THREAD_COUNT 10

using namespace std;
mutex m;
condition_variable cv;

int main(void){
    
    
    thread** t = new thread*[THREAD_COUNT];
    int i;
    for(i = 0; i < THREAD_COUNT; i++){
    
    
	    t[i] = new thread( [](int index){
    
    
	        unique_lock<mutex> lck(m);
	        cv.wait_for(lck, chrono::hours(1000));
	        cout << index << endl;}, i );
            
 	    this_thread::sleep_for( chrono::milliseconds(50) );
    }
    
    for(i = 0; i < THREAD_COUNT; i++){
    
    
	    lock_guard<mutex> _(m);
 	    cv.notify_one();
    }
    
    for(i = 0; i < THREAD_COUNT; i++){
    
    
 	    t[i]->join();
	    delete t[i];
    }
    delete t;
    
    return 0;
}

Después de compilar y ejecutar el programa y generar los resultados, puede ver que el orden de las variables de condición no está garantizado, es decir, es posible que la que llama a esperar primero no se despierte primero.

std :: promesa / futuro

Promise / future se puede usar para llevar a cabo una interacción de datos simple entre subprocesos sin considerar el problema de los bloqueos. El subproceso A guarda los datos en una variable de promesa, y otro subproceso B puede obtenerlos a través de get_future () de esta variable de promesa. Valor, cuando El subproceso A aún no ha asignado un valor en la variable de promesa, el subproceso B también puede esperar la asignación de la variable de promesa:

#include <thread>
#include <iostream>
#include <future>

using namespace std;

promise<string> val;

int main(void){
    
    
    thread ta([](){
    
    
	    future<string> fu = val.get_future();
	    cout << "waiting promise->future" << endl;
	    cout << fu.get() << endl;
    });
    
    thread tb([](){
    
    
	    this_thread::sleep_for( chrono::milliseconds(5000) );
	    val.set_value("promise is set");
    });
    
    ta.join();
    tb.join();
    
    return 0;
}

Una variable de futuro solo puede llamar a get () una vez. Si necesita llamar a get () varias veces, puede usar shared_future. También puede pasar excepciones entre hilos a través de promise / future.

std :: packaged_task

Si combina un objeto invocable con una promesa, es packaged_task, lo que puede simplificar aún más la operación:

#include <thread>
#include <iostream>
#include <mutex>
#include <future>

using namespace std;

static mutex g_mutex;

int main(void){
    
    
    auto run = [=](int index){
    
     
		{
    
    
	    	lock_guard<mutex> lck(g_mutex);
	    	cout << "tasklet " << index << endl;
		}
		this_thread::sleep_for( chrono::seconds(5) );
		return index * 1000;
    };
    
    packaged_task<int(int)> pt1(run);
    packaged_task<int(int)> pt2(run);
    thread t1( [&](){
    
    pt1(2);} );
    thread t2( [&](){
    
    pt2(3);} );

    int f1 = pt1.get_future().get();
    int f2 = pt2.get_future().get();
    cout << "task result=" << f1 << endl;
    cout << "task result=" << f2 << endl;

    t1.join();
    t2.join();
    
    return 0;
}

std :: async

También puede intentar combinar una packaged_task con un hilo, que es la función async (). Utilice la función async () para iniciar el código de ejecución y devolver un objeto futuro para guardar el valor de retorno del código. No necesitamos crear y destruir explícitamente subprocesos, etc., pero la implementación de la biblioteca C ++ 11 decide cuándo crear y destruir subprocesos, y crear varios subprocesos, etc., los ejemplos son los siguientes:

#include <thread>
#include <iostream>
#include <mutex>
#include <future>
#include <vector>

# define COUNT 1000000

using namespace std;

static long do_sum(vector<long> *arr, size_t start, size_t count){
    
    
    static mutex m;
    long sum = 0;
    
    for(size_t i = 0; i < count; i++){
    
    
	    sum += (*arr)[start + i];
    }
    
    {
    
    
	    lock_guard<mutex> lck(m);
	    cout << "thread " << this_thread::get_id() << ", count=" << count
	        << ", start="<< start << ", sum=" << sum << endl;
    }
    return sum;
}

int main(void){
    
    
    vector<long> data(COUNT);
    for(size_t i = 0; i < COUNT; i++){
    
    
        data[i] = random() & 0xff;
    }
    
    vector< future<long> > result;
    
    size_t ptc = thread::hardware_concurrency() * 2;
    for(size_t batch = 0; batch < ptc; batch++) {
    
    
	    size_t batch_each = COUNT / ptc;
	    if (batch == ptc - 1) {
    
    
	        batch_each = COUNT - (COUNT / ptc * batch);
	    }
	    result.push_back(async(do_sum, &data, batch * batch_each, batch_each));
    }

    long total = 0;
    for(size_t batch = 0; batch < ptc; batch++) {
    
    
	    total += result[batch].get();
    }
    cout << "total=" << total << endl;
    
    return 0;
}

En resumen, los anteriores son varios métodos de subprocesos múltiples diferentes, y los ejemplos utilizados son muy simples.
Para obtener detalles de los diferentes métodos específicos, aquí hay un enlace . Seguimiento, continúe aprendiendo.

Supongo que te gusta

Origin blog.csdn.net/qq_24649627/article/details/112557135
Recomendado
Clasificación