Programación multiproceso 2 de C ++ 11: comunicación multiproceso, sincronización de subprocesos, bloqueos

 

Programación multiproceso 1 en C++ 11: descripción general del subproceso múltiple

Programación multiproceso 2 de C ++ 11: comunicación multiproceso, sincronización de subprocesos, bloqueos

Programación multiproceso C ++ 11 tres: gestión de recursos de bloqueo y variables de condición 

Conceptos básicos de C/C++, creación de subprocesos Boost, sincronización de subprocesos 


2.0 Descripción general

        La sincronización de subprocesos es un mecanismo para la protección de datos . Los datos protegidos son datos compartidos. Los datos compartidos son un recurso al que acceden varios subprocesos juntos, es decir, una pieza de memoria, que es la misma pieza de memoria. Si hay dos subprocesos A y B escribe datos al mismo tiempo, A escribe 100, B escribe 200 y en este momento el hilo C lee esta memoria al mismo tiempo, entonces, ¿qué datos se leen? Definitivamente ocurrirá un error en este momento, porque si tres subprocesos usan esta memoria al mismo tiempo, se producirán errores, por lo que la sincronización de subprocesos no significa permitir que varios subprocesos hagan algo al mismo tiempo, sino hacer algo al mismo tiempo. Cuando suceden cosas, se ejecutan varios subprocesos en secuencia. En otras palabras, la sincronización de subprocesos no significa que los subprocesos se ejecuten en paralelo, sino que los subprocesos se ejecuten linealmente, de modo que se pueda garantizar la seguridad de los datos.
 
        Supongamos que hay 4 subprocesos A, B, C y D. Cuando el subproceso actual A accede a los recursos compartidos en la memoria, los otros subprocesos B, C y D no pueden operar en esta memoria hasta que el subproceso A acceda a los recursos compartidos en la memoria. memoria. Hasta que se acceda a la memoria del bloque, solo uno de B, C y D puede acceder a la memoria. Los dos restantes deben continuar bloqueando y esperando, y así sucesivamente, hasta que todos los subprocesos hayan completado la operación de esta memoria.

ejemplo:

#include <iostream>
#include <thread>
//#include <mutex>
//Linux make:  g++ -o main main3.c -lpthread
using namespace std;
//static mutex mut;

int i=0;

void thread_1(int n){
    while(n--){
       //mut.lock();
        i++;
       //mut.unlock();
    }
}

int main(){
	//n越大,i在不加锁的情况下出错越大
    int n=100000;
    thread th1(thread_1,n);
    thread th2(thread_1,n);
    th1.join();
    th2.join();
    cout<< i << endl;
    return 0;
}

 

        Si ejecuta este programa varias veces, encontrará que se generarán resultados diferentes, es decir, si no hay ningún error en el medio, los datos que generan deberían ser los mismos. Ahora es obvio que los dos subprocesos son Procesando el mismo espacio de memoria al mismo tiempo (el espacio de memoria actual es la variable global i), lo que provocó el error.

        Múltiples subprocesos comparten el tiempo y reutilizan intervalos de tiempo de la CPU, es decir, el subproceso A necesita tomar el intervalo de tiempo de la CPU. Quien tome el intervalo de tiempo lo ejecutará. Si el subproceso A toma el intervalo de tiempo de la CPU, la CPU comenzará a contar ¿De dónde provienen los datos? Estos datos se leen de la memoria física. Los datos en la memoria física se cargarán en los registros de la CPU y se procesarán a través de los registros. Generalmente, hay un caché entre la memoria física y la CPU, generalmente un caché de tercer nivel. ., los datos se transfieren desde la memoria física -> caché de nivel 3 -> caché de nivel 2 -> caché de nivel 1 -> registro de CPU. El almacenamiento en caché es para aumentar la velocidad. La velocidad de procesamiento de los registros es mucho más rápida que la velocidad de procesamiento de la memoria física. Hay Con un caché de este tipo, se puede mejorar la eficiencia del procesamiento de datos.

Sincrónicamente

        Para el problema de la confusión de datos cuando varios subprocesos acceden a recursos compartidos, se requiere la sincronización de subprocesos. Hay cuatro métodos de sincronización de subprocesos comúnmente utilizados: bloqueos mutex, bloqueos de lectura y escritura, variables de condición y semáforos. Los llamados recursos compartidos son variables a las que acceden varios subprocesos. Estas variables suelen ser variables de área de datos globales o variables de área de montón. Los recursos compartidos correspondientes a estas variables también se denominan recursos críticos.

         Cada subproceso en el área bloqueada solo puede ejecutar esta área por sí solo y no puede ejecutarse al mismo tiempo. Solo después de que el subproceso actual sale para desbloquearse, otros subprocesos pueden desbloquear el candado. Si tres están bloqueados, agarrarán el candado. , quienquiera lo agarra lo desbloqueará,


2.1 Análisis del estado de subprocesos múltiples y proceso de conmutación

Descripción del estado del hilo:

        Inicialización (Init): se está creando el hilo. (Es decir, cree un objeto de hilo y configure la función de devolución de llamada. El hilo está en el estado de inicialización, inicializando el espacio de memoria del hilo, etc. De hecho, no hay mucha intervención de código en esta parte, es decir, hay es en realidad un proceso entre la inicialización y el estado listo. Se consume tiempo, por lo que se utiliza un grupo de subprocesos más adelante para realizar un proceso para reducir el consumo de tiempo. Cuando varias memorias están listas, quedan listas.) Listo: no significa que se puede
         ejecutar inmediatamente y el subproceso está en la lista lista, esperando la programación de la CPU.
        En ejecución: el hilo se está ejecutando. Programado por la CPU.
        Bloqueado: El hilo está bloqueado y suspendido. El estado bloqueado incluye: pendiente (bloqueo de bloqueo, evento, semáforo, etc.), suspensión (pendiente activo), retraso (bloqueo de retraso), tiempo de espera (debido al tiempo de espera de bloqueo, evento, tiempo de semáforo, etc.). El estado de bloqueo significa que la programación de la CPU ya no está aquí y la programación de la CPU se abandona sin desperdiciar recursos.
        Salir: el hilo finaliza y espera a que el hilo principal reclame sus recursos del bloque de control.


2.2 El estado de competencia y la sección crítica introducen el código mutex

Condición de carrera: varios subprocesos leen y escriben datos compartidos simultáneamente.
Sección crítica: los fragmentos de código que leen y escriben datos compartidos
deben evitar estrategias de condiciones de carrera y proteger la sección crítica. Solo un subproceso puede ingresar a la sección crítica al mismo tiempo.


Para abordar los problemas en la descripción general 2.0:

En este momento, debe agregar un bloqueo mutex (mutex es un bloqueo mutex) y debe incluir el archivo de encabezado: #include <mutex>
mutex mutex estático; mux se llama variable mutex. Después de bloquear el recurso, otros subprocesos equivalen a esperar en la fila: bloquear
mux.lock es un bloqueo a nivel del sistema operativo. Solo hay un bloqueo mux mutex. Cuando un subproceso toma la iniciativa de procesarlo, primero debe aprovecharlo. el recurso de bloqueo, por lo que el hilo se apropia de él en un lado. Los recursos de la CPU toman los recursos de bloqueo.

#include <iostream>
#include <thread>
#include <mutex>	//需要包含的头文件
//Linux make:  g++ -o main main3.c -lpthread
using namespace std;

static mutex mut;	//添加互斥锁变量

int i=0;

void thread_1(int n){
    while(n--){
       mut.lock();	//获取锁资源,如果没有获得,则阻塞等待
        i++;
       mut.unlock();//释放锁
    }
}

int main(){
	//n越大,i在不加锁的情况下出错越大
    int n=100000;
    thread th1(thread_1,n);
    thread th2(thread_1,n);
    th1.join();
    th2.join();
    cout<< i << endl;
    return 0;
}

 

A partir de los resultados de varias ejecuciones, parece que no hay más errores, el resultado es el mismo cada vez y la sincronización del hilo es exitosa.


2.3 La razón por la que el hilo de pozo del bloqueo mutex no puede apoderarse de recursos

Idealmente, cuando un subproceso libera el recurso de bloqueo, los subprocesos posteriores se pondrán en cola para adquirir el recurso de bloqueo. Sin embargo, en la práctica, a veces un subproceso siempre ocupa el recurso y otros subprocesos se ponen en cola y nunca obtienen el recurso.

#include <thread>
#include <iostream>
#include <string>
#include <mutex>
//Linux -lpthread
using namespace std;
static mutex mux;
 
void ThreadMainMux(int i)
{
    for (;;)
    {
        mux.lock();
        cout << i << "[in]" << endl;
		this_thread::sleep_for(100ms);
        mux.unlock();
    }
}
int main(int argc, char* argv[])
{
    for (int i = 0; i < 3; i++)
    {
        thread th(ThreadMainMux, i + 1);
        th.detach();
    }
 
    getchar();
    return 0;
}

 

Encontrará que el resultado es que este hilo siempre ingresa y no todos los hilos se pueden mostrar de manera ideal. Este no es el resultado que queremos, las razones son las siguientes:

        En este código, cuando el subproceso 1 obtiene el recurso de bloqueo, el subproceso 2 y el subproceso 3 están en un estado bloqueado. Cuando el subproceso 1 está desbloqueado, lógicamente uno de los subprocesos 2 y 3 debería obtener el recurso de bloqueo inmediatamente, pero para el subproceso 1, digamos, cuando se desbloquea, vuelve a ingresar el bloqueo, es decir, vuelve a solicitar el bloqueo después del desbloqueo. Este bloqueo lo determina el kernel del sistema operativo para determinar si el recurso de bloqueo está ocupado. Cuando se desbloquea el subproceso 1, Por supuesto , los recursos de memoria no se liberan inmediatamente, porque nuestro sistema operativo no es un sistema operativo en tiempo real, y el tiempo entre el desbloqueo y el bloqueo puede ser de microsegundos, y la programación de la CPU debe detectar este recurso después de un período de tiempo.Cuando el hilo 1 Ingresa nuevamente al bloqueo inmediatamente después de desbloquearlo, y el sistema operativo no tiene tiempo de reaccionar. El sistema operativo pensará que el subproceso 1 ha tomado el recurso de bloqueo nuevamente. De hecho, no es así, sino porque el subproceso 1 ha vuelto a ingresar al bloqueo. antes de que sus recursos tengan tiempo de liberarse, por lo que ingresa nuevamente sin hacer cola, por lo que aquí es donde radica el problema. Por lo tanto, se debe agregar un retraso antes de desbloquear y bloquear para darle tiempo al sistema operativo para liberarse.

#include <thread>
#include <iostream>
#include <string>
#include <mutex>
//Linux -lpthread
using namespace std;
static mutex mux;
 
void ThreadMainMux(int i)
{
    for (;;)
    {
        mux.lock();
        cout << i << "[in]" << endl;
        //std::this_thread::sleep_for和sleep,没啥太大区别,都是表示当前线程休眠一段时间,
        //休眠期间不与其他线程竞争CPU,根据函数参数,等待相应时间时间。
        //只是一个是C的函数一个是c++的函数分别对应头文件 <unistd.h> 和 < thread >
        this_thread::sleep_for(100ms);
        mux.unlock();
        this_thread::sleep_for(1ms);
    }
}
int main(int argc, char* argv[])
{
    for (int i = 0; i < 3; i++)
    {
        thread th(ThreadMainMux, i + 1);
        th.detach();
    }
 
    getchar();
    return 0;
}


2.4 Bloqueo de tiempo de espera timed_mutex (para evitar un punto muerto a largo plazo) y bloqueo recursivo recursive_mutex

        Mutex no tiene un tiempo de espera de forma predeterminada, y cuando un subproceso está ocupado, otros subprocesos siempre se bloquean. Este método de código es conciso, pero dificulta la depuración posterior del código. Por ejemplo, accidentalmente escribimos una línea muerta en el código. Bloqueo, entonces, ¿cómo se encuentra este punto muerto durante la depuración? Registre el registro antes de cada bloqueo para ver si ha ingresado. Dichos costos de depuración son relativamente altos y no son fáciles de encontrar. Siempre que se agregue el tiempo de espera, se admite el tiempo de espera.

#include <thread>
#include <iostream>
#include <string>
#include <mutex>
//Linux -lpthread
using namespace std;
timed_mutex tmux;
 
void ThreadMainTime(int i){
    for (;;){
        if (!tmux.try_lock_for(chrono::milliseconds(500))){  //等待时间超过500ms就超时了
            cout << i << " try_lock_for timeout" << endl;
            continue;
        }
        cout << i << "[in]" << endl;
        this_thread::sleep_for(2000ms);  //假设要处理的业务的持续时间
        tmux.unlock();
        this_thread::sleep_for(1ms);     //防止某一个线程一直占用这个锁资源
    }
}
 
int main(int argc, char* argv[]){
    for (int i = 0; i < 3; i++){
        thread th(ThreadMainTime, i+1);
        th.detach();
    }
    getchar();
    return 0;
}

  

        Muchas empresas pueden usar el mismo candado y el mismo candado puede bloquearse varias veces. Si es un candado normal (bloqueo mutex), se generará una excepción la segunda vez que se bloquee el candado. Si no se detecta la excepción, entonces El programa se bloqueará y este bloqueo recursivo se puede bloquear varias veces. Aumentará el recuento de bloqueos del hilo actual en uno y el estado de bloqueo no cambiará. Habrá tantos desbloqueos como bloqueos, hasta que sea solo se publica cuando el recuento llega a cero, lo que puede evitar puntos muertos innecesarios. 

#include <thread>
#include <iostream>
#include <string>
#include <mutex>
//Linux -lpthread
using namespace std;
recursive_mutex rmux;

void Task1(){
    rmux.lock();
    cout << "task1 [in]" << endl;
    rmux.unlock();
}
void Task2(){
    rmux.lock();
    cout << "task2 [in]" << endl;
    rmux.unlock();
}
void ThreadMainRec(int i){
    for(;;){
        rmux.lock();
        Task1();
        cout << i << "[in]" << endl;
        this_thread::sleep_for(500ms);
        Task2();
        rmux.unlock();
        this_thread::sleep_for(1ms);
    }
}
int main(int argc, char* argv[]){
    for (int i = 0; i < 3; i++){
        thread th(ThreadMainRec, i + 1);
        th.detach();
    }
    getchar();
    return 0;
}


2.5 El bloqueo compartido Shared_mutex resuelve problemas de lectura y escritura

        El hilo actual debe ser mutuamente excluyente al escribir datos, es decir, otros hilos no pueden escribir ni leer. Mientras el hilo actual está leyendo, otros hilos solo pueden leer pero no escribir.
        Esto implica dos bloqueos, un bloqueo de lectura y un bloqueo de escritura.
Si este hilo es de solo lectura, entonces podemos usar un bloqueo de solo lectura. Sin embargo, cuando hay modificaciones involucradas en este hilo, primero necesitamos obtener el bloqueo de lectura, luego obtenga el bloqueo de escritura, luego modifíquelo y luego libérelo después de la modificación. Utilice este método para crear un recurso compartido.
        El bloqueo compartido contiene dos bloqueos, uno es el bloqueo compartido y el otro es el bloqueo mutex. Siempre que nadie bloquee el bloqueo mutex, el bloqueo compartido se devuelve inmediatamente. Siempre que alguien bloquee el bloqueo mutex, el bloqueo compartido no puede Tampoco se pueden ingresar otras cerraduras Mutex.
c++14 mutex temporizado compartidoshared_timed_mutex (el valor general predeterminado es compatible con C++14)
c++17 mutex compartidoshared_mutex Si el mutex solo se necesita al escribir y no al leer, ¿ cómo
usar bloqueos ordinarios?
El siguiente código solo puede ingresar un hilo para leer. En muchos escenarios comerciales, los recursos de la CPU no se utilizan por completo.

        El principio es que si un hilo está escribiendo, todos los demás hilos no pueden leer ni escribir. Si hay un hilo leyendo, otros hilos pueden leer, pero no escribir. Debe esperar a que todos los hilos de lectura terminen de leer antes de escribir, por lo que esto garantiza que el recurso no será escrito por varias personas y se producirán errores.
        Este es el candado compartido que vamos a utilizar. Primero se debe liberar el bloqueo de lectura. Si el bloqueo de lectura está bloqueado, no se puede ingresar el bloqueo mutex. Una vez que se ingresa el mutex, todos los demás hilos de lectura están esperando. Los bloqueos de escritura de otros subprocesos también están esperando y solo un subproceso puede escribir al mismo tiempo.

#include <thread>
#include <iostream>
#include <string>
#include <mutex>
#include <shared_mutex>
//Linux make: g++ -std=c++14 -o main main8.c -lpthread
using namespace std;

//shared_mutex smux;		//c++17  共享锁
shared_timed_mutex stmux;	//c++14  共享锁 
void ThreadRead(int i){
    for(;;){
        //stmux.lock_shared();共享锁,只要对方没有把互斥锁锁住,共享锁大家都可以进去。
        stmux.lock_shared();
        cout << i << " Read" << endl;
        this_thread::sleep_for(500ms);
        stmux.unlock_shared();
        this_thread::sleep_for(1ms);
    }
}
void ThreadWrite(int i){
    for(;;){
        stmux.lock_shared();
        //读取数据
        stmux.unlock_shared();
        stmux.lock(); //互斥锁 写入
        cout << i << " Write" << endl;
        this_thread::sleep_for(300ms);
        stmux.unlock();
        this_thread::sleep_for(1ms);
    }
}
int main(int argc, char* argv[]){
    for(int i = 0; i < 3; i++){
        thread th(ThreadWrite, i + 1);
        th.detach();
    }
    for(int i = 0; i < 3; i++){
        thread th(ThreadRead, i + 1);
        th.detach();
    }
    getchar();
    return 0;
}

Supongo que te gusta

Origin blog.csdn.net/qq_34761779/article/details/129226464
Recomendado
Clasificación