Diferencia entre revisiones de «Mecanismos de sincronización»

De Wiki de Sistemas Operativos
Saltar a: navegación, buscar
m (Pesimista: retirar referencia a E/S que puede confundir)
 
(No se muestran 43 ediciones intermedias de 14 usuarios)
Línea 1: Línea 1:
== Optimista==
+
= Optimistas =
Se considera que la frecuencia de acceso a un recurso compartido es baja, es decir, suponemos que la probabilidad de acceso simultanea a un recurso compartido es baja.
 
  
<font color="DarkGreen">/*Ejemplo de control optimista suponiendo 2 hilos, hx y hy.*/</font><br/>
+
Este mecanismo debe emplearse si el programador considera que la frecuencia de acceso a un recurso compartido es baja, es decir, que supone que la probabilidad de coincidencia de dos o más procesos al recurso compartido es baja.
  <font color="#0000FF">int</font> compartida = 1, tmp;
 
      retry:
 
            tmp = compartida;        <font color="DarkGreen">/* anoto */</font>
 
            tmp++;                    <font color="DarkGreen">/* actualizo temporal */</font>
 
            <font color="#0000FF">if</font>(compartida+1 != tmp)
 
                  <font color="#0000FF">goto</font> retry;          <font color="DarkGreen">/* compruebo si la variable compartida ha sido modificada mientras operaba con el temporal */</font>
 
  
Los inconvenientes de este tipo de mecanismo es su elevado recurso de memoria, ya que se debe tener una copia del recurso compartido y efectuar la comprobación, y su alto recurso de procesamiento, al tener que volver a realizar la operación. Por esto último, puede darse el caso de que un hilo esté sin progresar durante un tiempo indefinido, si se da el caso de que siempre que se le agote el tiempo de procesador asignado debido a que otro hilo modifique el recurso compartido.
+
El siguiente ejemplo muestra el código ejecutado por dos hilos: h<sub>x</sub> y h<sub>y</sub> de un mismo proceso.
  
==Pesimista==
+
<source lang="c">
Se considera que la frecuencia de acceso al recurso compartido es alta.
+
int tabla[3] = { 100, 101, 102 };  /* tabla compartida por ambos hilos hx y hy. */
En este mecanismo disponemos de tres partes :
 
*Un protocolo de entrada : en el cual se restringe el permiso de acceso para las variables compartidas.
 
*Una sección crítica : donde se realizan todas las operaciones con las variables compartidas.
 
*Un protocolo de salida : en el cual se restablece el permiso de acceso para las variables compartidas.
 
  
(Toda operación con una variable compartida necesita un protocolo de entrada y otro de salida)
+
/* actividad del hilo */
 +
void *actividad_hilo(void *ptr)
 +
{
 +
        int i;
  
<font color="DarkGreen">/*Ejemplo de control pesimista suponiendo 2 hilos, hx y hy*/</font><br/>
+
        while(1) {
  <font color="#0000FF">int</font> compartida = 1;
+
            for (i=0; i<3; i++) {
        no_permito_acceso_variable_compartida();       <font color="DarkGreen">/* protocolo de entrada */</font>
+
                  actualiza_tabla(i);
                compartida++;                       <font color="DarkGreen">/* sección crítica */</font>
+
                  sleep((random() % 10) + 1);   /* hilo pasa a estado bloqueado durante 1 a 10 segundos. */
        permito_acceso_a_variable_compartida();   <font color="DarkGreen">/* protocolo de salida */</font>
+
            }
 +
        }
 +
        return NULL;
 +
}
  
'''¿Cómo implementamos el protocolo de entrada y salida en el control de concurrencia pesimista?'''
+
int main(void)
 +
{
 +
        pthread_t hilo1, hilo2;
  
Interrumpiendo la conmutación, desactivando las interrupciones, seguidamente ejecutando la sección crítica y finalmente permitiendo el acceso a las variables compartidas y activando las interrupciones.
+
        pthread_create(&hilo1, NULL, actividad_hilo, NULL);  /* creación hilo<sub>x</sub>. */
 +
        pthread_create(&hilo2, NULL, actividad_hilo, NULL);  /* creación hilo<sub>y</sub>. */
 +
        pthread_join(hilo1, NULL);
 +
        pthread_join(hilo2, NULL);
 +
}
  
'''¿Cómo implementar el control de concurrencia pesimista?'''
+
void actualiza_tabla(int i) {
 +
        int tmp;
 +
retry:
 +
        tmp = tabla[i];            /* almaceno el valor de la variable compartida en una temporal. */
 +
        tmp++;                    /* actualizo la varible temporal. */
 +
        if (tabla[i]+1 != tmp) {  /* compruebo si la variable compartida ha sido modificada */
 +
            goto retry;            /* mientras operaba con la variable temporal. */
 +
        }
 +
        tabla[i]=tmp;
 +
}
 +
</source>
  
*Espera ocupada/activa : cerrojos. Se comprueba continuamente la condición que permite franquear el protocolo de entrada
+
Los inconvenientes de este tipo de mecanismo son dos:
*Espera no ocupada/no activa : semáforos, monitores y mensajes. Se pasa a estado bloqueado cuando no se puede franquear el protocolo de entrada.
+
 
 +
* Se consume más memoria, pues hay que realizar una copia del recurso compartido para efectuar la actualización.
 +
* En situaciones de coincidencia, se tiene que volver a realizar la operación, por tanto, se desperdician recursos de procesamiento.
 +
 
 +
En base al ejemplo anterior, hay que garantizar que la operación de comprobación y actualización es '''atómica'''. Se dice que una operación es '''atómica''' cuando es indivisible, es decir, que no quedamos en una situación intermedia al ejecutar dicha operación. Los procesadores incluyen una función llamada ''Test and Set'' (TAS) que nos permite ejecutar dos órdenes de manera '''atómica'''. Al ser una instrucción máquina el procesador nos garantiza que ningún proceso va a interrumpirla, evitando así la '''condición de carrera'''. Por ejemplo en x86 tenemos la instrucción ''LOCK BTS'' (Bit Test and Set) que nos permite añadir una cadena de bits para que se ejecute de manera '''atómica'''.
 +
 
 +
Como ventaja, este tipo de mecanismo nos ofrece un mayor grado de paralelismo, por tanto, una mayor tasa de transferencia, siempre que se emplee en una situación donde la frecuencia de concurrencia sea baja.
 +
 
 +
Por último, si el programador ha empleado una aproximación optimista para una situación en la que la frecuencia de coincidencia es alta, podría darse el caso de que un hilo no progrese en su ejecución de manera indefinida, al tener que volver a reintentar la actualización una y otra vez hasta conseguirlo sin que haya concurrencia.
 +
 
 +
 
 +
Un mecanismo de sincronización optimista funciona de la siguiente manera:
 +
 
 +
* Paso 1. Realiza una copia del estado en el que está el recurso compartido.
 +
* Paso 2. Opera con esa copia.
 +
* Paso 3. Compruebo si mi copia alterada coincide con el original aplicándole también el tratamiento.
 +
 
 +
= Pesimistas =
 +
Este mecanismo debe emplearse si se considera que la frecuencia de acceso al recurso compartido es alta.
 +
 
 +
En este mecanismo disponemos de tres partes:
 +
 
 +
* El protocolo de entrada, en el que se emplea un mecanismo que no permite continuar con la ejecución si otro u otros procesos están accediendo al recurso compartido.
 +
* La sección crítica, en el que se realizan las operaciones pertinentes con el recurso compartido.
 +
* El protocolo de salida, en el que se vuelve a permitir el acceso al recurso compartido.
 +
 
 +
El siguiente ejemplo muestra cómo se implementa el control de concurrencia pesimista para 2 hilos, h<sub>x</sub> y h<sub>y</sub>:
 +
 
 +
<source lang="c">
 +
  int compartida = 1;
 +
  protocolo_de_entrada();    /* no permito acceso a variable compartida. */
 +
  compartida++;              /* sección crítica: actualización de la variable compartida. */
 +
  protocolo_de_salida();      /* vuelvo a permitir acceso a la variable compartida. */
 +
</source>
 +
 
 +
Los protocolos de entrada y salida son, generalmente, operaciones costosas en términos de recursos de procesamiento, pues requieren el uso de instrucciones atómicas cuyo tiempo de ejecución es alto.
 +
 
 +
Por último, se podría emplear un control de concurrencia pesimista para resolver un problema que se resuelve con un control de concurrencia optimista, pero no al revés.
 +
 
 +
== Implementación del control de concurrencia pesimista ==
 +
 
 +
El control de concurrencia pesimista se puede implementar de dos formas mediante:
 +
 
 +
* Espera ocupada o activa, mediante '''cerrojos'''. En este mecanismo se comprueba continuamente la condición que permite franquear en el protocolo de entrada, por tanto, el proceso permanece en estado activo comprobando continuadamente la condición que le permite progresar en su ejecución, tiempo que está siendo desaprovechado por el procesador.
 +
* Espera no ocupada o bloqueante, mediante '''semáforos''', '''monitores''' o '''mensajería'''. Estos mecanismos hacen que el proceso pase a estado bloqueado cuando no se puede franquear el protocolo de entrada, por tanto, al quedar un proceso que no puede progresar en estado bloqueado, el planificador de procesos tiene que seleccionar a otro proceso.
 +
 
 +
 
 +
 
 +
Nota aclarativa: Definición de ''espera ocupada o activa''
 +
 
 +
Esperar a que algo ocurra realizando continuas comprobaciones.
 +
Ejemplo:
 +
            …
 +
            variable= 0;
 +
            while(variable==0);
 +
            …
 +
 
 +
 
 +
 
 +
5.3[[Cerrojos | Cerrojos]]

Revisión actual del 17:29 2 abr 2020

Optimistas

Este mecanismo debe emplearse si el programador considera que la frecuencia de acceso a un recurso compartido es baja, es decir, que supone que la probabilidad de coincidencia de dos o más procesos al recurso compartido es baja.

El siguiente ejemplo muestra el código ejecutado por dos hilos: hx y hy de un mismo proceso.

int tabla[3] = { 100, 101, 102 };  /* tabla compartida por ambos hilos hx y hy. */

/* actividad del hilo */
void *actividad_hilo(void *ptr)
{
        int i;

        while(1) {
            for (i=0; i<3; i++) {
                  actualiza_tabla(i);
                  sleep((random() % 10) + 1);    /* hilo pasa a estado bloqueado durante 1 a 10 segundos. */
            }
        }
        return NULL;
}

int main(void)
{
        pthread_t hilo1, hilo2;

        pthread_create(&hilo1, NULL, actividad_hilo, NULL);  /* creación hilo<sub>x</sub>. */
        pthread_create(&hilo2, NULL, actividad_hilo, NULL);  /* creación hilo<sub>y</sub>. */
        pthread_join(hilo1, NULL);
        pthread_join(hilo2, NULL);
}

void actualiza_tabla(int i) {
        int tmp;
retry:
        tmp = tabla[i];            /* almaceno el valor de la variable compartida en una temporal. */
        tmp++;                     /* actualizo la varible temporal. */
        if (tabla[i]+1 != tmp) {   /* compruebo si la variable compartida ha sido modificada */
            goto retry;            /* mientras operaba con la variable temporal. */
        }
        tabla[i]=tmp;
}

Los inconvenientes de este tipo de mecanismo son dos:

  • Se consume más memoria, pues hay que realizar una copia del recurso compartido para efectuar la actualización.
  • En situaciones de coincidencia, se tiene que volver a realizar la operación, por tanto, se desperdician recursos de procesamiento.

En base al ejemplo anterior, hay que garantizar que la operación de comprobación y actualización es atómica. Se dice que una operación es atómica cuando es indivisible, es decir, que no quedamos en una situación intermedia al ejecutar dicha operación. Los procesadores incluyen una función llamada Test and Set (TAS) que nos permite ejecutar dos órdenes de manera atómica. Al ser una instrucción máquina el procesador nos garantiza que ningún proceso va a interrumpirla, evitando así la condición de carrera. Por ejemplo en x86 tenemos la instrucción LOCK BTS (Bit Test and Set) que nos permite añadir una cadena de bits para que se ejecute de manera atómica.

Como ventaja, este tipo de mecanismo nos ofrece un mayor grado de paralelismo, por tanto, una mayor tasa de transferencia, siempre que se emplee en una situación donde la frecuencia de concurrencia sea baja.

Por último, si el programador ha empleado una aproximación optimista para una situación en la que la frecuencia de coincidencia es alta, podría darse el caso de que un hilo no progrese en su ejecución de manera indefinida, al tener que volver a reintentar la actualización una y otra vez hasta conseguirlo sin que haya concurrencia.


Un mecanismo de sincronización optimista funciona de la siguiente manera:

  • Paso 1. Realiza una copia del estado en el que está el recurso compartido.
  • Paso 2. Opera con esa copia.
  • Paso 3. Compruebo si mi copia alterada coincide con el original aplicándole también el tratamiento.

Pesimistas

Este mecanismo debe emplearse si se considera que la frecuencia de acceso al recurso compartido es alta.

En este mecanismo disponemos de tres partes:

  • El protocolo de entrada, en el que se emplea un mecanismo que no permite continuar con la ejecución si otro u otros procesos están accediendo al recurso compartido.
  • La sección crítica, en el que se realizan las operaciones pertinentes con el recurso compartido.
  • El protocolo de salida, en el que se vuelve a permitir el acceso al recurso compartido.

El siguiente ejemplo muestra cómo se implementa el control de concurrencia pesimista para 2 hilos, hx y hy:

   int compartida = 1;
   protocolo_de_entrada();     /* no permito acceso a variable compartida. */
   compartida++;               /* sección crítica: actualización de la variable compartida. */
   protocolo_de_salida();      /* vuelvo a permitir acceso a la variable compartida. */

Los protocolos de entrada y salida son, generalmente, operaciones costosas en términos de recursos de procesamiento, pues requieren el uso de instrucciones atómicas cuyo tiempo de ejecución es alto.

Por último, se podría emplear un control de concurrencia pesimista para resolver un problema que se resuelve con un control de concurrencia optimista, pero no al revés.

Implementación del control de concurrencia pesimista

El control de concurrencia pesimista se puede implementar de dos formas mediante:

  • Espera ocupada o activa, mediante cerrojos. En este mecanismo se comprueba continuamente la condición que permite franquear en el protocolo de entrada, por tanto, el proceso permanece en estado activo comprobando continuadamente la condición que le permite progresar en su ejecución, tiempo que está siendo desaprovechado por el procesador.
  • Espera no ocupada o bloqueante, mediante semáforos, monitores o mensajería. Estos mecanismos hacen que el proceso pase a estado bloqueado cuando no se puede franquear el protocolo de entrada, por tanto, al quedar un proceso que no puede progresar en estado bloqueado, el planificador de procesos tiene que seleccionar a otro proceso.


Nota aclarativa: Definición de espera ocupada o activa

Esperar a que algo ocurra realizando continuas comprobaciones. Ejemplo:

           …
           variable= 0;
           while(variable==0);
           …


5.3 Cerrojos