[Sistema Operativo] Programación Linux - Creación y uso de multihilos II (Uso de secciones críticas, mutex y semáforos)

        Este artículo tiene un total de 4505 palabras y el tiempo estimado de lectura es de 8 minutos.

Tabla de contenido

El concepto de sección crítica. 

Simular tareas multiproceso

Sincronización de hilos

exclusión mutua

Bloqueo y desbloqueo mutex

Cantidad de señal


El concepto de sección crítica. 

        En el ejemplo anterior, solo intentamos crear 1 hilo para manejar la tarea. A continuación, intentemos crear varios hilos.

        Sin embargo, primero debemos ampliar un concepto: "zona crítica".

        Una sección crítica se refiere a un fragmento de programa que accede a recursos compartidos (como dispositivos compartidos o memoria compartida) , y varios subprocesos no pueden acceder a estos recursos compartidos al mismo tiempo. Cuando un subproceso ingresa a una sección crítica, otros subprocesos o procesos deben esperar (por ejemplo: método de espera de espera limitada) . Se deben implementar algunos mecanismos de sincronización en los puntos de entrada y salida de la sección crítica para garantizar que se utilicen estos recursos compartidos. se obtiene usando, por ejemplo: semáforo .

        Es fácil causar problemas cuando se ejecutan varios subprocesos al mismo tiempo, este problema se centra en el acceso a recursos compartidos.

        Dependiendo de si la función causará problemas en la sección crítica cuando se ejecute, los tipos de funciones se pueden dividir en dos categorías:

  • Función segura para subprocesos
  • Función insegura para subprocesos

        Las funciones seguras para subprocesos no causan problemas cuando las llaman varios subprocesos simultáneamente , mientras que las funciones no seguras para subprocesos causan problemas cuando las llaman varios subprocesos.

expandir:

        Ya sea Linux o Windwos, no necesitamos distinguir entre funciones seguras para subprocesos y funciones no seguras para subprocesos, porque al diseñar funciones no seguras para subprocesos, los desarrolladores también diseñan funciones seguras para subprocesos con las mismas funciones.

        Los nombres de las funciones seguras para subprocesos generalmente se agregan con el sufijo _r en la función . Sin embargo, si todos escribimos expresiones de función de esta manera en la programación, será muy problemático. Por esta razón, podemos definirlo antes de declarar el encabezado. archivo._macro REENTRANT .

        Si busca una experiencia de escritura de código más rápida, puede agregar -D_REENTRANT al compilar los parámetros de entrada en lugar de hacer referencia a la macro _REENTRANT al escribir código .

Simular tareas multiproceso

        A continuación, simulemos un escenario para reflejar este problema. El siguiente es un código de muestra:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

#define THREAD_NUM 100

void *thread_inc(void *arg);
void *thread_des(void *arg);

long num = 0;

int main(int argc, char *argv[])
{
    pthread_t thread_id[THREAD_NUM];
    int i;

    for (i = 0; i < THREAD_NUM; i++)
    {
        if (i % 2)
            pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
        else
            pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
    }

    for (i = 0; i < THREAD_NUM; i++)
        pthread_join(thread_id[i], NULL);

    printf("result: %ld \n", num);
    return 0;
}

void *thread_inc(void *arg)
{
    for (int i = 0; i < 100000; i++)
        num += 1;
    return NULL;
}

void *thread_des(void *arg)
{
    int i;
    for (int i = 0; i < 100000; i++)
        num -= 1;
    return NULL;
}

resultado de la operación:

        Obviamente, el resultado no es el valor "0" deseado y el valor de salida cambia con el tiempo.

        Entonces, ¿qué causa que aparezcan valores tan poco realistas?

        He aquí una situación:

        Cuando el subproceso A inicia el acceso a la variable λ=98, el subproceso B también inicia el acceso, por lo que en este momento tanto el subproceso A como el B obtienen el valor de λ=98. Después de que el hilo calculó +1 el valor, obtuvo 99 e inició una solicitud de actualización a la variable de recurso. Sin embargo, el hilo B también hizo la misma operación en este momento y, según el valor λ = 98 que también se obtuvo antes, entonces el resultado final es que A calculó λ = 99, B también calculó λ = 99 y el último valor actualizado también fue 99, pero en realidad debería ser 100.

        En resumen, el motivo de este tipo de problema es que existe una "diferencia horaria" al acceder y procesar el mismo recurso al mismo tiempo, lo que provoca que el resultado final se desvíe de la situación real.

        Después de comprender el motivo, este problema se puede resolver fácilmente, es decir, limitar los permisos de lectura y escritura de los recursos a los que se accede al mismo tiempo y sincronizar los subprocesos.

Sincronización de hilos

        Para la sincronización de subprocesos, debe confiar en las dos definiciones conceptuales de " mutex " y " semáforo ".

exclusión mutua

        Mutex se utiliza para limitar el acceso simultáneo de varios subprocesos, resuelve principalmente el problema del acceso de sincronización de subprocesos y es un mecanismo de "bloqueo".

        El mutex también tiene funciones especiales en la biblioteca pthread.h para creación y destrucción, echemos un vistazo a su estructura de funciones:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t * mutex , const pthread_mutexattr_t * attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);

//成功时返回 0 ,失败时返回其他值。

/* 参数定义
 
    mutex: 创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值。
    attr:  传递即将创建的互斥量属性,没有需要指定的属性时可以传递NULL。

        Además, si no necesita configurar atributos mutex específicos, puede inicializarlo usando la macro PTHREAD_MUTEX_INITIALIZER . El ejemplo es el siguiente:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

        Sin embargo, es mejor utilizar la función pthread_mutex_init para la inicialización, porque es difícil localizar el punto de error al depurar macros, y la configuración de los atributos mutex de pthread_mutex_init también es más intuitiva y controlable.

Bloqueo y desbloqueo mutex

        Las dos funciones mencionadas anteriormente solo se utilizan para creación y destrucción. Las más críticas son las dos funciones operativas de bloqueo y desbloqueo . Su estructura es la siguiente:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);

// 成功时返回 0 ,失败时返回其他值 。

        La función que debe llamarse antes de ingresar a la sección crítica es pthread_mutex_lock . Si se descubre que otros subprocesos han ingresado a la sección crítica al llamar a esta función, la función pthread_mutex_lock no devolverá un valor en este momento a menos que el subproceso interno llame a pthreaed_mutex_unlock . función para salir de la sección crítica.

        El diseño estructural general de la sección crítica es el siguiente:

pthread_mutex_lock(&mutex);
//临界区的开始
//..........
//..........
//临界区的结束
pthread_mutex_unlock(&mutex);

        Preste especial atención al hecho de que pthread_mutex_unlock () y pthread_mutex_lock () generalmente están en una relación de par. Si el bloqueo no se libera después de que el hilo sale de la sección crítica, otros subprocesos que esperan ingresar a la sección crítica no podrán obtener deshacerse del estado de bloqueo y eventualmente convertirse en un estado de "punto muerto".

        A continuación, intentemos utilizar un mutex para resolver los problemas anteriores.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

#define THREAD_NUM 100

void *thread_inc(void *arg);
void *thread_des(void *arg);

long num = 0;
pthread_mutex_t mutex;

int main(int argc, char *argv[])
{
    pthread_t thread_id[THREAD_NUM];
    int i;

    pthread_mutex_init(&mutex, NULL);

    for (i = 0; i < THREAD_NUM; i++)
    {
        if (i % 2)
            pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
        else
            pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
    }

    for (i = 0; i < THREAD_NUM; i++)
        pthread_join(thread_id[i], NULL);

    printf("result: %ld \n", num);
    pthread_mutex_destroy(&mutex);
    return 0;
}

void *thread_inc(void *arg)
{
    pthread_mutex_lock(&mutex);
    // ↓临界区代码执行块
    for (int i = 0; i < 100000; i++)
        num += 1;
    // ↑临界区代码执行块
    pthread_mutex_unlock(&mutex);
    return NULL;
}
void *thread_des(void *arg)
{
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < 100000; i++)
    {
        // ↓临界区代码执行块
        num -= 1;
        // ↑临界区代码执行块
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

resultado de la operación:

        El resultado finalmente es correcto ~

        Lo que necesita especial atención es que al diseñar el área de bloqueo, todos deben considerar cuidadosamente los límites y confirmar exactamente el punto de ejecución del código que debe "bloquearse" y el punto de "liberación" que se puede finalizar. Esto puede evitar llamadas frecuentes a " operaciones de bloqueo". " y "desbloqueo", mejorando así la eficiencia de ejecución del código del sistema operativo.

Cantidad de señal

        Los semáforos son similares a los mutex y también son un método de sincronización de subprocesos. Generalmente, los semáforos se representan mediante 0 y 1 binarios, por lo que este tipo de semáforo también se denomina "semáforo binario".

        Los siguientes son los métodos de creación y destrucción de semáforos:

#include <semaphore.h>
int sem_init(sem_t * sem , int pshared, unsigned int value);
int sem_destroy(sem_ t * sem);

//成功时返回0,失败时返回其他值

/* 参数含义
    
    sem: 创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值。
    pshared: 传递0时,创建只允许1个进程内部使用的信号量。传递其他值时,创建可由多个进程共享的信号量。
    value: 指定新创建的信号量初始值。
*/

        Al igual que los mutex, existen funciones de "bloqueo" y "desbloqueo".

#include <semaphore.h>
int sem_post(sem_ t * sem);
int sem_wait(sem_t * sem);

//成功时返回0,失败时返回其他值。

/* 参数含义

    sem: 传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait信号量减1。 

*/

        Al llamar a la función sem_init , el sistema operativo crea un objeto de semáforo e inicializa el valor del semáforo. El valor es +1 cuando se llama a la función sem_post y -1 cuando se llama a la función sem_wait .

        Cuando un hilo llama a la función sem_wait para hacer que el valor del semáforo sea 0, el hilo entrará en el estado de bloqueo. Si otros hilos llaman a la función sem_post en este momento, el hilo previamente bloqueado podrá escapar del estado de bloqueo e ingresar al sección crítica.

        La estructura de la sección crítica de un semáforo es generalmente la siguiente (suponiendo que el valor inicial del semáforo es 1):

sem_wait(&sem); //进入到临界区后信号量为0
// 临界区的开始
// ..........
// ..........
// 临界区的结束
sem_post(&sem); // 信号量变为1

        Los semáforos se utilizan generalmente para resolver problemas de sincronización con orden fuerte en tareas de subprocesos.

Supongo que te gusta

Origin blog.csdn.net/weixin_42839065/article/details/131532784
Recomendado
Clasificación