[Sistema operativo] sincronización de subprocesos, exclusión mutua de subprocesos, operación atómica

Hilo mutex

Introducir

Veamos un fragmento de código multiproceso. Este es un ejemplo clásico de venta de boletos de tren. La estación de tren de Xi'an ahora tiene 10 boletos para el oeste de Beijing. Hay 3 ventanillas para comprar boletos:

// 销售火车票
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 10;

// 每个窗口都执行的售票操作,假设每个窗口每次只卖出 1 张
void* SellTicket(void*);

int main()
{
    // 有 3 个售票窗口在售票
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, SellTicket, "窗口1");
    pthread_create(&tid2, NULL, SellTicket, "窗口2");
    pthread_create(&tid3, NULL, SellTicket, "窗口3");
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    return 0;
}

void*
SellTicket(void* arg)
{
    char* id = (char*) arg;
    while (1)
    {
        if (ticket > 0) 
        {
            sleep(1);  // 售票员小姐姐操作一下
            --ticket;
            printf("%s 售出 1 张, 剩余 %d 张\n", id, ticket);
        }
        else
        {
            printf("票售罄了!\n");
            break;  // 关闭售票窗口
        }
    }
}

¿No hay nada malo con el código y no hay problemas con dos pasos?
Compilar gcc 3-销售火车票.c -lpthread
ejecutar ./a.out
Inserte la descripción de la imagen aquí
para ver el resultado asustado, ¿por qué no tenía la otra taquilla siguen vendiendo?
El culpable esEjecución preventiva de hilos

Razón

¿Cuántos pasos se necesitan para poner el elefante en el refrigerador?
Abra la puerta del refrigerador, meta el elefante y cierre la puerta del refrigerador.

Entonces --ticket da unos pocos pasos?
Bajo la arquitectura de von Neumann:
lea el ticket en el registro, el circuito convertirá el valor del registro a -1 y volverá a colocar el valor en la memoria correspondiente al ticket.

Dado que los subprocesos se programan de manera preventiva, puede ocurrir la siguiente situación:
Supongamos que ahora hay 10 tickets.
Hilo 1: ticket-> register [ticket: 10, register: 10]
[Thread 1 CPU time slice to save site, switch to thread 2 ejecución]
Hilo 2: ticket-> register [ticket: 10, register: 10]
thread 2: valor de registro -1 [ticket: 10, registro: 9]
Subproceso 2: registro-> ticket [ticket: 9, registro: 9]
[El subproceso 2 se completa, cambie a la ejecución del subproceso 1, reanude la escena]
Subproceso 1: registre Valor -1 [ticket: 10, registro: 10]
Tema 1: registro-> ticket [ticket: 9, registro: 9]
¡Finalmente! ! ! Se vendieron un total de 2 boletos en las dos ventanas, ¡pero el boleto era 9! ! !

Resolver

Primero comprendamos algunos conceptos:
recursos críticos : los recursos compartidos por secuencias de ejecución de subprocesos múltiples se denominan recursos críticos. Por ejemplo, el anterior ticketes un recurso crítico.
Sección crítica : el código que accede a los recursos críticos dentro de cada hilo se llama sección crítica. Como el de arriba --ticket.
Atomicidad : una operación que no será interrumpida por ningún mecanismo de programación. La operación tiene solo dos estados, ya sea completada o no completada.

Hilo mutex

Exclusión mutua : en cualquier momento, la exclusión mutua garantiza que solo un flujo de ejecución ingrese al área crítica y acceda a los recursos críticos, generalmente protegiendo los recursos críticos.

Cuando el hilo 1 realiza la operación de tres pasos de "instalar el elefante en el refrigerador", no permite que otros hilos roben la ejecución correcta del hilo 1. El punto profesional es que cuando el código ingresa a la sección crítica para su ejecución, otros hilos no pueden ingresar a la sección crítica.
Para lograr la exclusión mutua, entonces necesitamos algo para identificar si un hilo está utilizando recursos críticos actualmente. Esta cosa se llama mutex (mutexe).

Echemos un vistazo al código modificado:

// 销售火车票
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 10;
pthread_mutex_t mutex;

void* SellTicket(void*);

int main()
{
    pthread_mutex_init(&mutex, NULL);

    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, SellTicket, "窗口1");
    pthread_create(&tid2, NULL, SellTicket, "窗口2");
    pthread_create(&tid3, NULL, SellTicket, "窗口3");
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    pthread_mutex_destroy(&mutex);
    return 0;
}

void* SellTicket(void* arg)
{
    char* id = (char*) arg;
    while (1)
    {
        pthread_mutex_lock(&mutex);
        if (ticket > 0) 
        {
            sleep(1);
            --ticket;
            printf("%s 售出 1 张, 剩余 %d 张\n", id, ticket);
            pthread_mutex_unlock(&mutex);
            sched_yield();  // 测试:放弃 CPU 执行权
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            printf("票售罄了!\n");
            break;
        }
    }
}

Cuando un hilo entra en la sección crítica, lo bloquea y lo desbloquea después de salir de la sección crítica.
Si el bloqueo se usa cuando otros hilos se ejecutan aquí, espere.

Inserte la descripción de la imagen aquí

Mutex es una especie de mutex Cerradura pendienteUna vez que un proceso está bloqueado, otros procesos no logran adquirir el bloqueo y se bloquean (ingrese a la cola de espera del sistema operativo).
Cuando el sistema operativo libera y programa el bloqueo, puede continuar ejecutándose.
Mutexes puede garantizar la seguridad del hilo, pero la eficiencia del programa final se ve afectada, además de esto puede haber problemas más serios.Punto muerto.
Entonces debe tenerse en cuenta que el estado de bloqueo y desbloqueo del mutex también debe ser una operación atómica.

Para lograr la operación de bloqueo de exclusión mutua, la mayoría de las arquitecturas proporcionan instrucciones de intercambio o intercambio. El propósito de esta instrucción es intercambiar datos entre registros y unidades de memoria. Dado que solo hay una instrucción, la atomicidad está garantizada, incluso en plataformas multiprocesador También hay ciclos de bus para acceder a la memoria. Cuando se ejecuta la instrucción de intercambio en un procesador, la instrucción de intercambio del otro procesador solo puede esperar el ciclo del bus.

Otros tipos de cerraduras:
Bloqueos pesimistas: cada vez que obtiene datos, siempre le preocupa que otros hilos modifiquen los datos, por lo que agregará bloqueos (bloqueos de lectura, bloqueos de escritura, bloqueos de fila, etc.) antes de recuperar datos. Cuando otros hilos quieran acceder a los datos, Bloqueado y colgado.
Bloqueo optimista: cada vez que se obtienen datos, siempre es optimista que los datos no sean modificados por otros subprocesos, por lo que no están bloqueados. Pero antes de actualizar los datos, determinará si se han modificado otros datos antes de la actualización. Hay dos métodos principales: mecanismo de número de versión y operación CAS.
Operación CAS: Cuando los datos necesitan ser actualizados, determine si el valor actual de la memoria es igual al valor obtenido previamente. Si son iguales, se actualizan con nuevos valores. Si no es igual, fallará, y si falla, se volverá a intentar. Generalmente es un proceso de rotación, es decir, reintento continuo.
¿Bloqueo de giro, bloqueo justo, bloqueo injusto?

Hilo de sincronización

Sincronización : la sincronización controla el orden de ejecución entre subprocesos y evita que se ejecuten de forma preventiva.
Bajo la premisa de garantizar la seguridad de los datos, permitir que los hilos accedan a recursos críticos en un cierto orden para evitar efectivamente los problemas de hambre se llama sincronización.


Tomemos una castaña en la vida: en un juego de baloncesto, hay dos acciones de pase y clavado. Suponiendo que el pase y el clavado son hechos por dos personas, entonces debes tener una secuencia, pasar el balón primero y luego clavarlo.
Supongamos que el pase dura 789789 ms y la volcada tarda 123123 ms.

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

// 传球动作
void* ThreadEntry1(void* args) {
    (void) args;
    while (1) {
        printf("传球\n");
        usleep(789789);
    }
    return NULL;
}

// 扣篮动作
void* ThreadEntry2(void* args) {
    (void)args;
    while (1) {
        printf("-扣篮\n");
        usleep(123123);
    }
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, ThreadEntry1, NULL);
    pthread_create(&tid2, NULL, ThreadEntry2, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

Ejecute dos pasos:
puede ver que sumerge sin obtener la pelota. En este caso, necesita controlar el orden. Primero debe obtener la pelota antes de poder sumergir.
Inserte la descripción de la imagen aquí
Agregamos control al código anterior:

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

// 互斥锁
pthread_mutex_t mutex;
// 条件变量
pthread_cond_t cond;

// 传球动作
void* ThreadEntry1(void* args) {
    (void) args;
    while (1) {
        printf("传球\n");
        // 传球过去了,通知一下
        pthread_cond_signal(&cond);
        usleep(788789);
    }
    return NULL;
}

// 扣篮动作
void* ThreadEntry2(void* args) {
    (void)args;
    while (1) {
        // 首先得等待球传过来
        // 一直等到球传过来
        pthread_cond_wait(&cond, &mutex);
        printf("-扣篮\n");
        usleep(123123);
    }
    return NULL;
}

int main() {
    // 初始化 cond 和 mutex
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, ThreadEntry1, NULL);
    pthread_create(&tid2, NULL, ThreadEntry2, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

Corre a correr:
Inserte la descripción de la imagen aquí
AtencionThreadEntry2 ejecución de pthread_cond_wait()cuándo va a hacer tres acciones:
1, primero liberar el bloqueo;
2, a la espera de la condición cond listo;
3, vuelva a adquirir el bloqueo, lleve a cabo las siguientes operaciones.
Una o dos de estas operaciones deben ser atómicas, de lo contrario, puede perder otros mensajes de notificación de subprocesos, lo que provocará una estupidez esperando aquí.
En la mayoría de los casos, las variables de condición deben usarse con bloqueos mutex.

¿Por qué pthread_cond_wait necesita un mutex?
La espera condicional es un medio de sincronización entre subprocesos. Si solo hay un subproceso, las condiciones no se cumplen y la espera no se cumplirá, por lo que debe haber un subproceso a través de algunas operaciones para cambiar las variables compartidas, de modo que las condiciones que no se cumplieron previamente Para estar satisfecho, y notificación amigable esperando el hilo en la variable de condición. Las condiciones no se cumplirán repentinamente sin ninguna razón e inevitablemente implicarán cambios en los datos compartidos. Por lo tanto, debe protegerse con un mutex. Sin bloqueos de exclusión mutua, los datos compartidos no pueden obtenerse y modificarse de manera segura.

  1. Por ejemplo, si ambos hilos necesitan acceder a un recurso compartido, ¿debe bloquearse el recurso compartido?
  2. Si la función de espera adquiere el bloqueo primero, ¿qué sucede si el otro hilo de señalización necesita adquirir el bloqueo, entonces el hilo que necesita recibir la señal libera el bloqueo durante la función de espera y espera a que el hilo de señalización señale después de acceder a recursos críticos? .
  3. Si, antes de esperar la función, se libera el bloqueo, entonces el hilo de señalización envía la señal. Bueno, no ha llegado el momento de ingresar la señal de función de espera, entonces esto no esperará para siempre.
  4. Por lo tanto, las acciones de desbloqueo y espera son atómicas, por lo que esta función requiere el mutex. Luego, dentro de la función, el programador usará algunas instrucciones atómicas para completar estas dos operaciones.

EOF

Se han publicado 98 artículos originales · 91 alabanzas · Más de 40,000 visitas

Supongo que te gusta

Origin blog.csdn.net/Hanoi_ahoj/article/details/105272260
Recomendado
Clasificación