Concurrencia
Concurrencia
Referencias
Descarga en PDF
LECCIÓN 1 de 3
Concurrencia
Concurrencia
Como usuarios de una computadora, queremos que esta ejecute la mayor
cantidad de procesos en la menor cantidad de tiempo posible. Pero, si
recordamos algunos conceptos de arquitectura del computador, sabremos
que un procesador es capaz de ejecutar un proceso a la vez. La utilización de
un ambiente de multiprocesador o de sistemas distribuidos mejora el
rendimiento porque varios procesos pueden ejecutarse simultáneamente.
Los procesos e hilos pueden requerir transferir datos entre ellos, depender
uno de otro y, además, competir por el uso del procesador.
Gestionar estas tres características y evitar interferencias entre procesos o
hilos son funciones que le corresponden al sistema operativo.
Problemas clásicos de concurrencia
Problema de los lectores-escritores
En este problema …existe un determinado objeto, …que puede
ser un archivo, un registro dentro de un archivo, etc., que va a ser
utilizado y compartido por una serie de procesos concurrentes.
Algunos de estos procesos sólo van a acceder al objeto sin
modificarlo, mientras que otros van a acceder al objeto para
modificar su contenido. Esta actualización implica leerlo,
modificar su contenido y escribirlo. A los primeros procesos se
los denomina lectores y a los segundos se los denomina
escritores. En este tipo de problemas existe una serie de
restricciones que han de seguirse:
Sólo se permite que un escritor tenga acceso al objeto al mismo
tiempo. Mientras el escritor esté accediendo al objeto, ningún otro
proceso lector ni escritor podrá acceder a él.
Se permite, sin embargo, que múltiples lectores tengan acceso al
objeto, ya que nunca van a modificar el contenido del mismo.
En este tipo de problemas es necesario disponer de servicios de
sincronización que permitan a los procesos lectores y escritores
sincronizarse adecuadamente en el acceso al objeto. (Silva,
2015, pp. 158 y 159).
Figura 1: Recurso
Fuente: Silva, 2015, p. 159.
Problema del productor-consumidor
El problema del productor-consumidor es uno de los problemas
más habituales que surge cuando se programan aplicaciones
utilizando procesos concurrentes. En este tipo de problemas,
uno o más procesos, que se denominan productores, generan
cierto tipo de datos que son utilizados o consumidos por otros
procesos, que se denominan consumidores. Un claro ejemplo
de este tipo de problemas es el de un compilador. El compilador
hace las funciones de productor al generar el código
ensamblador que consumirá proceso ensamblador para generar
el código máquina.
En esta clase de problemas, es necesario disponer de algún
mecanismo de comunicación que permita a los procesos
productor y consumidor intercambiar información. Ambos
procesos, además, deben sincronizar su acceso al mecanismo
de comunicación para que la interacción entre ellos no sea
problemática: cuando el mecanismo de comunicación se llene,
el proceso productor deberá quedar bloqueado hasta que haya
hueco para seguir insertando elementos. A su vez, el proceso
consumidor deberá quedarse bloqueado cuando el mecanismo
de comunicación este vacío, ya que en este caso no podrá
continuar su ejecución al no disponer de información a consumir.
Por tanto, este tipo de problema requiere servicios para que los
procesos puedan comunicarse y servicios para que se
sincronicen a la hora de acceder al mecanismo de
comunicación. (Silva, 2015, p. 158).
Figura 2: Mecanismo de la comunicación
Fuente: Sistemas Operativos Modernos, 2016, [Link]
Ejecución concurrente de procesos
Los procesos que se ejecutan de forma concurrente en un
sistema se pueden clasificar como procesos independientes o
cooperantes. …Tanto si los procesos son independientes como
cooperantes, pueden producirse una serie de interacciones
entre ellos. Estas interacciones pueden ser de dos tipos:
Interacciones motivadas porque los procesos comparten o
compiten por el acceso a recursos físicos o lógicos. Esta
situación aparece en los distintos tipos de procesos
anteriormente comentados. Por ejemplo, dos procesos
totalmente independientes pueden competir por el acceso a
disco. En este caso, el sistema operativo deberá encargarse de
que los dos procesos accedan ordenadamente sin que se cree
ningún conflicto. Esta situación también aparece cuando varios
procesos desean modificar el contenido de un registro de una
base de datos. Aquí es el gestor de la base de datos el que se
tendrá que encargar de ordenar los distintos accesos al registro.
Interacción motivada porque los procesos se comunican y
sincronizan entre sí para alcanzar un objetivo común. Por
ejemplo, un compilador se puede construir mediante dos
procesos: el compilador propiamente dicho, que se encarga de
generar código ensamblador, y el proceso ensamblador, que
obtiene código en lenguaje máquina a partir del ensamblador.
Estos dos tipos de interacciones obligan al sistema operativo a
incluir mecanismos y servicios que permitan la comunicación y
la sincronización entre procesos. (Sistemas Operativos
Modernos, 2016, [Link]
Condiciones de carrera
El primer problema con el que un sistema operativo debe lidiar son las
denominadas condiciones de carrera. Este inconveniente surge porque
existen componentes compartidos por varios procesos, como, por ejemplo,
archivos o sectores de memoria. Podría darse que dos procesos interfieran
entre sí al querer utilizar un mismo sector de memoria: el que llega primero
ocupa este sector porque no es consciente de que el otro también quiere
usarlo. Pero si el otro proceso tampoco sabe que el primero está a punto de
usar ese sector, puede, también, considerarse vacío y usarlo.
La solución parece simple: prohibir que estos dos procesos hagan uso de
algo compartido, lo que se denomina exclusión mutua. Pero en realidad no lo
es tanto, ya que también es necesario que los procesos cooperen entre sí y
compartan información.
Se denomina región o sección crítica a la parte de un programa que tiene que
hacer uso de algo compartido, por ejemplo, la memoria. En la Figura 3 se
observa cómo funciona este esquema. Cuando el proceso A ingresa en su
región crítica y, un momento después, el proceso B intenta también hacerlo,
este último se bloquea. Esto asegura que solo un proceso esté en su región
crítica.
Existen diferentes opciones para lograr este objetivo.
Figura 3: Exclusión mutua con regiones críticas
Fuente: Tanenbaum, 2009, p. 120.
Deshabilitación de interrupciones
Cuando un proceso ingresa en su región crítica, puede deshabilitar por
hardware las interrupciones. De esta forma, ningún otro proceso podrá
interrumpir al procesador. Son muchas las desventajas de este método: si,
por algún motivo, un proceso no vuelve a habilitar las interrupciones, el
sistema no puede continuar operando. Además, en un ambiente con más de
un procesador, un proceso solo puede bloquear a uno, entonces, otro proceso
podría ejecutarse en otro procesador y acceder al mismo sector de memoria.
Variables de candado
Este método se basa en software, en lugar de en hardware. Podemos utilizar
la variable denominada candado, para que sea consultada por cada proceso
que requiere entrar a la región crítica. En ese caso, establecemos que, si no
hay otro proceso en la región crítica, el candado estará en 0, y que, si lo hay,
estará en 1. De esta forma, cada proceso puede consultar al candado antes
de ingresar en la región crítica.
Puede ocurrir una condición de carrera si un proceso se ejecuta y coloca el
candado en 1, justo luego de que otro proceso consultara el candado, lo
encontrara en 0 y entrara a su región crítica.
Alternancia estricta
Este método consiste en examinar de forma permanente una variable similar
a la explicada en la variable de candado. La ventaja es que, de esta forma, un
proceso se asegura no ingresar a su región crítica si otro está en la propia. La
desventaja es que la examinación constante consume tiempo de CPU, por lo
que debe utilizarse cuando se puede determinar que la espera no será larga.
Semáforos
Un semáforo funciona de la siguiente manera: la variable denominada
Semáforo toma el valor 0 si no guardó ninguna señal de despertar, y valores
positivos cuando hay una o más señales de despertar pendientes.
Las señales de despertar permiten que se active el proceso que actualmente
está dormido (es decir, bloqueado) porque no puede entrar en su región
crítica.
El funcionamiento del semáforo involucra las operaciones Down (bajar) y Up
(subir). La operación Down supone que la variable Semáforo es mayor a 1, y
lo que hace es decrementar en 1. En cambio, si el proceso encuentra la
variable en 0, se llama a Sleep (pasa a estado Dormido). Solo un proceso
puede usar este proceso, y los demás deben esperar su turno, para evitar
posibles condiciones de carrera.
La operación Up permite que, si el semáforo tenía más de un proceso
inactivo, se seleccione uno de estos y se complete la operación Down a partir
de despertarlo.
Para conocer cómo es posible solucionar el problema del
productor/consumidor mediante la utilización de semáforos, consulta la
sección 2.3 del libro de Tanenbaum.
Mutex
Los mutex son versiones simplificadas de los semáforos. Empleados con
eficiencia y facilidad, resultan muy útiles para paquetes de hilos que se
implementan, en su totalidad, en el plano del usuario (Tanenbaum, 2005).
Supongamos que tenemos dos hilos (h1 y h2) que leen la variable “valor” que
inicialmente tiene un valor de 5. Si ambas desean aumentar el valor de esta
variable puede ocurrir lo siguiente:
el hilo h1 lee valor (5);
el hilo h2 lee valor (5);
el hilo h2 aumenta en 3 valor (5+3=8) y guarda;
el hilo h1 aumenta en 2 valor (5+2=7) y guarda.
Una vez finalizado, este “valor” es igual a 7 cuando debería ser igual a 10.
Para evitar esto empleamos como mecanismo de sincronización los mutex.
Los mutex nos ayudarán a bloquear los accesos a datos; mientras un
proceso ligero (hilo) esté accediendo a una sección crítica, otro proceso no
podrá acceder a ella y esperará a que liberen el mutex para acceder. Las
variables mutex son del tipo pthread_mutex_t. Los métodos para emplear los
mutex son los siguientes:
int pthread_mutex_init(pthread_mutex_t *mutex,
pthread_mutexattr_t * attr): Inicializa el mutex;
int pthread_mutex_destroy(pthread_mutex_t *mutex): destruye el
mutex (lo elimina de la memoria);
int pthread_mutex_lock(pthread_mutex_t *mutex): bloquea el mutex
si no lo tiene nadie. Si alguien tiene bloqueado el mutex el proceso
espera hasta que el proceso que lo tiene bloqueado lo libere;
int pthread_mutex_unlock(pthread_mutex_t *mutex): libera el mutex.
En conclusión, un mutex es el mecanismo de sincronización de procesos
ligeros más sencillo y eficiente. Los mutex se emplean para obtener acceso
exclusivo a recursos compartidos y para asegurar la exclusión mutua sobre
secciones críticas. Sobre un mutex se pueden realizar dos operaciones
atómicas básicas.
Lock: intenta bloquear el mutex. Si el mutex ya está bloqueado por
otro proceso, el proceso que realiza la operación se bloquea. En
caso contrario, se bloquea el mutex sin bloquear el proceso.
Unlock: desbloquea el mutex. Si existen procesos bloqueados en él,
se desbloqueará a uno de ellos, que será el nuevo proceso que
adquiera el mutex. La operación unlock sobre un mutex debe
ejecutarla el proceso ligero que adquirió con anterioridad el mutex
mediante la operación lock.
Dado que las operaciones lock y unlock son atómicas, solo un proceso
conseguirá bloquear el mutex y podrá continuar su ejecución dentro de la
sección crítica. El segundo proceso se bloqueará hasta que el primero libere
el mutex mediante la operación unlock.
Figura 4: Mutex en Pthreads
Fuente: Mutex en Pthreads (POSIX Threads)
En la Figura 4 están presentes las llamadas para bloquear y desbloquear un
mutex, y otras disponibles cuando se utilizan mutexes en Pthreads.
Tabla 1: Llamadas al sistema para mutex
Fuente: Tanenbaum, 2009, p. 133.
PThreads, además, utiliza variables de condición. Lo que estas variables
hacen es permitir que el productor se bloquee cuando no hay más espacio
disponible en el buffer, algo que no es posible realizar solo con el mutex.
Así como existen variables para la utilización de mutex, existen otras
relacionadas con las variables de condición. Por ejemplo, Pthread_cond_init,
que crea una variable de condición.
Variables condicionales
Una variable condicional es una variable de sincronización asociada a un
mutex que se utiliza para bloquear a un proceso hasta que ocurra algún
suceso. Las variables condicionales tienen dos operaciones atómicas:
“esperar” y “señalizar”.
c_wait: bloquea al proceso que ejecuta la llamada y lo expulsa del
mutex dentro del cual se ejecuta y al que está asociado la variable
condicional, permitiendo que algún otro proceso adquiera el mutex.
El bloqueo del proceso y la liberación del mutex se realizan de
forma atómica.
c_signal: desbloquea uno o varios procesos suspendidos en la
variable condicional. El proceso que se despierta compite de nuevo
por el mutex.
Figura 5: c_wait, c_signal
Fuente: elaboración propia.
Monitores
La operación con semáforos y mutexes puede derivar en un problema grave,
conocido como deadlock o interbloqueo. Si esto ocurre, tanto el productor
como el consumidor se bloquean y no se continúan realizando trabajos. Este
tipo de problemas se deben a errores en la programación de las aplicaciones.
Los monitores facilitan a los programadores la escritura de programas
correctos, es decir, evitan que se cometan errores que conduzcan a
deadlocks. Pero ¿qué es, en definitiva, un monitor?
Un monitor es una colección de procedimientos, variables y estructuras de
datos que se agrupan en un tipo especial de módulo o paquete. Los procesos
pueden llamar a los procedimientos en un monitor cada vez que lo desean,
pero no pueden acceder de manera directa a las estructuras de datos
internas del monitor desde procedimientos declarados fuera de este
(Tanenbaum, 2009).
En la Figura 6 se observa la estructura de un monitor. Solo un proceso puede
estar en el monitor, mientras que los restantes estarán bloqueados a la
espera de su turno. Si un proceso se bloquea dentro del monitor, pasará a
una cola de espera para poder volver a ingresar.
Figura 6: Estructura de un monitor
Fuente: Stallings, 2005, p. 230.
La ventaja que los monitores tienen sobre los semáforos es que todas las
funciones de sincronización están, en ellos, confinadas. Por lo tanto, es más
fácil comprobar que la sincronización se haya realizado correctamente y
detectar los errores. Es más, una vez que un monitor se ha programado
correctamente, el acceso al recurso protegido será correcto para todo
acceso desde cualquier proceso.
En cambio, en el caso de los semáforos, el acceso al recurso será correcto
solo si todos los procesos que acceden al recurso han sido programados
correctamente (Stallings, 2005).
Barreras
A diferencia de los métodos anteriores, este se basa en grupos de procesos,
y no en procesos individuales, ya que algunas aplicaciones se dividen en fase
y, para que un proceso pueda avanzar a la siguiente fase, todos los demás
deben estar listos para hacerlo. Entonces, los procesos se bloquean hasta
que el resto esté listo, como si existiera una barrera de contención.
C O NT I NU A R
LECCIÓN 2 de 3
Referencias
Stallings, W. (2005). Sistemas Operativos. España: Pearson Education.
Silva, M. (2015). Sistemas Operativos. Buenos Aires, Argentina: Alfaomega
Sistemas Operativos Modernos. (16 de junio de 2016). Problemas clásicos
de comunicación entre procesos. En Sistemas Operativos Modernos.
Recuperado den [Link]
[Link]
Tanenbaum, A. (2009). Sistemas Operativos Modernos. México: Pearson
Education.
LECCIÓN 3 de 3
Descarga en PDF
Módulo 2 - Lectura [Link]
289.9 KB