Bloqueo mutex para sincronización de subprocesos de Linux

Requisito previo: subprocesos múltiples en C

La sincronización de subprocesos se define como un mecanismo que garantiza que dos o más procesos o subprocesos simultáneos no ejecuten simultáneamente algún segmento de programa en particular conocido como sección crítica. El acceso de los procesos a la sección crítica se controla mediante el uso de técnicas de sincronización. Cuando un subproceso comienza a ejecutar la sección crítica (un segmento serializado del programa), el otro subproceso debe esperar hasta que finalice el primer subproceso. Si no se aplican las técnicas de sincronización adecuadas, puede provocar una condición de carrera en la que los valores de las variables pueden ser impredecibles y variar según los tiempos de los cambios de contexto de los procesos o subprocesos.

Problemas de sincronización de subprocesos
Un código de ejemplo para estudiar problemas de sincronización:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
  
pthread_t tid[2];
int counter;
  
void* trythis(void* arg)
{
    unsigned long i = 0;
    counter += 1;
    printf("\n Job %d has started\n", counter);
  
    for (i = 0; i < (0xFFFFFFFF); i++)
        ;
    printf("\n Job %d has finished\n", counter);
  
    return NULL;
}
  
int main(void)
{
    int i = 0;
    int error;
  
    while (i < 2) {
        error = pthread_create(&(tid[i]), NULL, &trythis, NULL);
        if (error != 0)
            printf("\nThread can't be created : [%s]", strerror(error));
        i++;
    }
  
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
  
    return 0;
}

¿Cómo compilar el programa anterior?
Para compilar un programa multihilo usando gcc, necesitamos vincularlo con la biblioteca pthreads. El siguiente es el comando utilizado para compilar el programa.

gfg@ubuntu:~/$ gcc filename.c -lpthread

En este ejemplo, se crean dos subprocesos (trabajos) y en la función de inicio de estos subprocesos, se mantiene un contador para obtener los registros sobre el número de trabajo que se inicia y cuándo se completa.

Producción :

Job 1 has started
Job 2 has started
Job 2 has finished
Job 2 has finished

Problema: De los dos últimos registros, se puede ver que el registro ‘ Trabajo 2 ha finalizado ‘ se repite dos veces mientras que no se ve ningún registro para ‘ Trabajo 1 ha finalizado ‘.

¿Por qué se ha producido?
Al observar de cerca y visualizar la ejecución del código, podemos ver que:

  • El registro ‘El trabajo 2 ha comenzado ‘ se imprime justo después de ‘ El trabajo 1 ha comenzado ‘, por lo que se puede concluir fácilmente que mientras el subproceso 1 estaba procesando, el programador programó el subproceso 2.
  • Si tomamos como verdadera la suposición anterior, entonces el valor de la variable ‘ contador ‘ se incrementó nuevamente antes de que terminara el trabajo 1.
  • Entonces, cuando el trabajo 1 realmente terminó, entonces el valor incorrecto del contador produjo el registro ‘ El trabajo 2 ha terminado ‘ seguido por el ‘ El trabajo 2 ha terminado ‘ para el trabajo real 2 o viceversa, ya que depende del programador.
  • Entonces vemos que no es el registro repetitivo sino el valor incorrecto de la variable ‘contador’ el problema.
  • El problema real fue el uso de la variable ‘contador’ por un segundo subproceso cuando el primer subproceso la estaba usando o estaba a punto de usarla.
  • En otras palabras, podemos decir que la falta de sincronización entre los subprocesos al usar el ‘contador’ de recursos compartidos causó los problemas o, en una palabra, podemos decir que este problema ocurrió debido a un ‘problema de sincronización’ entre dos subprocesos.
  • Cómo resolverlo ?

    La forma más popular de lograr la sincronización de subprocesos es mediante Mutexes .

    exclusión mutua

    • Un Mutex es un bloqueo que establecemos antes de usar un recurso compartido y lo liberamos después de usarlo.
    • Cuando se establece el bloqueo, ningún otro subproceso puede acceder a la región de código bloqueada.
    • Entonces vemos que incluso si el subproceso 2 está programado mientras que el subproceso 1 no terminó de acceder al recurso compartido y el código está bloqueado por el subproceso 1 usando mutexes, entonces el subproceso 2 ni siquiera puede acceder a esa región de código.
    • Esto asegura el acceso sincronizado de los recursos compartidos en el código.

    Funcionamiento de un mutex

  1. Supongamos que un subproceso ha bloqueado una región de código usando mutex y está ejecutando ese fragmento de código.
  2. Ahora, si el programador decide hacer un cambio de contexto, todos los demás subprocesos que están listos para ejecutar la misma región se desbloquean.
  3. Solo uno de todos los subprocesos llegaría a la ejecución, pero si este subproceso intenta ejecutar la misma región de código que ya está bloqueada, volverá a dormir.
  4. El cambio de contexto tendrá lugar una y otra vez, pero ningún subproceso podrá ejecutar la región bloqueada del código hasta que se libere el bloqueo mutex.
  5. El bloqueo mutex solo lo liberará el subproceso que lo bloqueó.
  6. Por lo tanto, esto garantiza que una vez que un subproceso haya bloqueado un fragmento de código, ningún otro subproceso podrá ejecutar la misma región hasta que el subproceso que lo bloqueó lo desbloquee.

Por lo tanto, este sistema asegura la sincronización entre los subprocesos mientras se trabaja en recursos compartidos.

Se inicializa un mutex y luego se logra un bloqueo llamando a las siguientes dos funciones: la primera función inicializa un mutex y, a través de la segunda función, se puede bloquear cualquier región crítica en el código.

  1. int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr) : crea una exclusión mutua, referenciada por mutex, con atributos especificados por attr. Si attr es NULL, se utiliza el atributo mutex predeterminado (NO RECURSIVO).

    Valor devuelto
    Si tiene éxito, pthread_mutex_init() devuelve 0 y el estado de la exclusión mutua se inicializa y desbloquea.
    Si no tiene éxito, pthread_mutex_init() devuelve -1.

  2. int pthread_mutex_lock(pthread_mutex_t *mutex) : bloquea un objeto mutex, que identifica un mutex. Si el mutex ya está bloqueado por otro subproceso, el subproceso espera a que el mutex esté disponible. El subproceso que ha bloqueado un mutex se convierte en su propietario actual y sigue siendo el propietario hasta que el mismo subproceso lo desbloquee. Cuando el mutex tiene el atributo de recursivo, el uso del candado puede ser diferente. Cuando este tipo de exclusión mutua se bloquea varias veces por el mismo subproceso, se incrementa un recuento y no se publica ningún subproceso en espera. El subproceso propietario debe llamar a pthread_mutex_unlock() la misma cantidad de veces para reducir el conteo a cero.

    Valor devuelto
    Si tiene éxito, pthread_mutex_lock() devuelve 0.
    Si no tiene éxito, pthread_mutex_lock() devuelve -1.

El mutex se puede desbloquear y destruir llamando a las siguientes dos funciones: la primera función libera el bloqueo y la segunda función destruye el bloqueo para que no se pueda usar en ningún lugar en el futuro.

  1. int pthread_mutex_unlock(pthread_mutex_t *mutex) : Libera un objeto mutex. Si uno o más subprocesos están esperando para bloquear el mutex, pthread_mutex_unlock() hace que uno de esos subprocesos regrese de pthread_mutex_lock() con el objeto mutex adquirido. Si no hay subprocesos esperando la exclusión mutua, la exclusión mutua se desbloquea sin propietario actual. Cuando el mutex tiene el atributo de recursivo, el uso del candado puede ser diferente. Cuando este tipo de mutex está bloqueado varias veces por el mismo subproceso, el desbloqueo disminuirá el conteo y no se publicará ningún subproceso en espera para continuar ejecutándose con el bloqueo. Si el recuento se reduce a cero, se libera el mutex y, si hay algún subproceso en espera, se publica.

    Valor devuelto
    Si tiene éxito, pthread_mutex_unlock() devuelve 0.
    Si no tiene éxito, pthread_mutex_unlock() devuelve -1

  2. int pthread_mutex_destroy(pthread_mutex_t *mutex) : elimina un objeto mutex, que identifica un mutex. Mutexes se utilizan para proteger los recursos compartidos. mutex se establece en un valor no válido, pero se puede reinicializar mediante pthread_mutex_init().

    Valor devuelto
    Si tiene éxito, pthread_mutex_destroy() devuelve 0.
    Si no tiene éxito, pthread_mutex_destroy() devuelve -1.

mutex
An example to show how mutexes are used for thread synchronization

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
  
pthread_t tid[2];
int counter;
pthread_mutex_t lock;
  
void* trythis(void* arg)
{
    pthread_mutex_lock(&lock);
  
    unsigned long i = 0;
    counter += 1;
    printf("\n Job %d has started\n", counter);
  
    for (i = 0; i < (0xFFFFFFFF); i++)
        ;
  
    printf("\n Job %d has finished\n", counter);
  
    pthread_mutex_unlock(&lock);
  
    return NULL;
}
  
int main(void)
{
    int i = 0;
    int error;
  
    if (pthread_mutex_init(&lock, NULL) != 0) {
        printf("\n mutex init has failed\n");
        return 1;
    }
  
    while (i < 2) {
        error = pthread_create(&(tid[i]),
                               NULL,
                               &trythis, NULL);
        if (error != 0)
            printf("\nThread can't be created :[%s]",
                   strerror(error));
        i++;
    }
  
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_mutex_destroy(&lock);
  
    return 0;
}

En el código anterior:

  • Un mutex se inicializa al comienzo de la función principal.
  • El mismo mutex está bloqueado en la función ‘trythis()’ mientras se usa el ‘contador’ de recursos compartidos.
  • Al final de la función ‘trythis()’ se desbloquea el mismo mutex.
  • Al final de la función principal, cuando se completan ambos subprocesos, se destruye el mutex.

Producción :

Job 1 started
Job 1 finished
Job 2 started
Job 2 finished

Así que esta vez están presentes los registros de inicio y finalización de ambos trabajos. Entonces, la sincronización de subprocesos se llevó a cabo mediante el uso de Mutex.

Referencias:
Sincronización (informática)
Bloqueo (informática)

Este artículo es una contribución de Kishlay Verma . Si le gusta GeeksforGeeks y le gustaría contribuir, también puede escribir un artículo usando contribuya.geeksforgeeks.org o envíe su artículo por correo a contribuya@geeksforgeeks.org. Vea su artículo que aparece en la página principal de GeeksforGeeks y ayude a otros Geeks.

Escriba comentarios si encuentra algo incorrecto o si desea compartir más información sobre el tema tratado anteriormente.

Publicación traducida automáticamente

Artículo escrito por GeeksforGeeks-1 y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *