Home - ProgProcesosYServicios/Practicas-2 GitHub Wiki
Capítulo 2
Recursos compartidos y sincronización
There is always a well-known solution to every human problem - ueat, plausible, and wrong. H.L. Mencken
RESUMEN: En este capítulo veremos los problemas que las hebras pueden sufrir a la hora de compartir recursos, y varias de alternativas clásicas para (intentar) resolverlos. Se pone de manifiesto la dificultad de la programación concurrente.
En el capítulo anterior hablamos sobre la concurrencia, y vimos las características principales de la clase Thread, la herramienta proporcionada para crear flujos de ejecución independientes.
Disponer de la posibilidad de crear hebras es el primer paso para conseguir concurrencia. Al ñnal del capítulo nos acercamos ligeramente a la sincronización de las hebras, haciendo uso de yield() en un primer momento, y de suspend() y resume() después.
Antes de seguir por ese camino, merece la pena analizar con un poco de cuidado los principales problemas de la programación concurrente. Cuando dos hebras (o dos procesos) colaboran entre sí, lo normal es que, al menos, compartan recursos, como variables comunes, archivos o bases de datos. Las hebras accederán a esos recursos para consultarlos o para modificarlos, y deberán ser conscientes de que, al mismo tiempo, hay otros que están haciendo lo mismo.
Un ejemplo clásico de recurso es “un dato” (ya sea una variable, una base de datos o un fichero en disco) que algunas hebras querrán leer, y otras querrán escribir. En un determinado momento puede haber muchas hebras que lean el dato sin que se produzcan problemas; sin embargo la escritura debe realizarse con más cuidado, porque mientras una hebra escribe ninguna otra debería leer ni escribir el mismo dato.
En la programación secuencial habitual esto no es un problema, pero en la programación concurrente sí. Si tenemos varias hebras que quieren acceder al mismo recurso en un sistema multiprocesador, necesitaremos algún tipo de sincronización. Lo mismo ocurre si estamos en un sistema monoprocesador en el que el uso del recurso puede exceder el cuanto de tiempo concedido a la ejecución de cada hebra.
En general, existen los siguientes problemas:
-
Necesidad de exclusión mutua: se debe garantizar que las hebras utilizan los recursos compartidos de una en una, y no simultáneamente. Si una hebra esta usando un determinado recurso y otra lo necesita, tendrá que esperar. Las porciones de código en los que una hebra hace uso de un recurso que debe ser utilizado en exclusión mutua se conocen como secciones criticas.
-
Interbloqueos: la necesidad de exclusión mutua puede ocasionar inter- bloqueos si dos hebras necesitan, simultáneamente, más de un recurso. Cada hebra podía reservar uno de los recursos y quedar esperando indefinidamente a que el otro quede libre.
-
Inaniciónuna hebra no debería esperar indefinidamente a que se le conceda el uso de un recurso. Hay dos variantes:
-
Inanición en presencia de contención: un recurso muy solicitado es concedido una y otra vez a una o varias de las hebras solicitantes, y no es entregada nunca a otra. Es la más conocida.
-
Inanición en ausencia de contención: un recurso está libre, y aun así una hebra que desea utilizarlo no recibe el permiso para hacerlo.
- Coherencia de los datos: si usarios recursos deben guardar entre si algúntipo de relación (por ejemplo, una variable debe tener siempre el doble del valor de otra), se debe garantizar que las operaciones sobre ellas sean “atómicas” desde el punto de vista del resto de las hebras para no sufrir actualizaciones perdidas.
Todos estos problemas de la programación concurrente indican que se debe tener mucho cuidado al hacer uso de recursos compartidos. De otro modo se podrían sufrir condiciones de carrera (en inglés race condition). Se produce una condición de carrera entre dos procesos o hebras cuando el resultado final depende del orden en el que se ejecuten. En un programa secuencial, con un orden total, esto no es un problema; pero las instrucciones de los programas concurrentes tienen un orden parcial, y si el resultado de la ejecución depende del orden (arbitrario) escogido por el sistema Operativo entonces el programa no será correcto.
Un ejemplo muy simple de condición de carrera es la compartición de una variable global, n, en dos hebras. Si ambas ejecutan:
++n;
ambas tendrán que leer el valor de la variable de memoria, hacer la suma, y volcar el resultado. Si la ejecución de las lecturas se entrelaza, una de las actualizaciones se perderá, algo que no ocurrirá si no se entrelazan.
La necesidad de exclusión mutua, y el riesgo de interbloqueos o inanición lo sufren no sólo hebras que cooperan entre sí, sino también procesos independientes qne compiten en el uso de recursos. Un ejemplo clásico es el uso de una impresora por parte de dos procesos diferentes e independientes. Entre ellos no se conocen, pero ambos compiten, sin saberlo, por un recurso que debe usarse en exclusión mutua. Es el sistema operativo quién debe lidiar con la asignación de recursos, pero esto demuestra que, efectivamente, los sistemas operativos son aplicaciones concurrentes que deben tener cuidado de los mismos problemas que cualquier otra (y muchos más).
Práctica 2.1: Condiciones de carrera
Práctica 2.2: Comportamiento dela memoria: atributos volatile
Práctica 2.3: Exclusión mutua: el algoritmo de Dekker (primer intento)
Práctica 2.4: Exclusión mutua: el algoritmo de Dekker (segundo intento)
Práctica 2.5: Exclusión mutua: el algoritmo de Dekker (tercer intento)
Práctica 2.6: Exclusión mutua: el algoritmo de Dekker (cuarto intento)
Práctica 2.7: Exclusión mutua: el algoritmo de Dekker: solución final
Práctica 2.8: Exclusión mutua: el algoritmo de Peterson
Práctica 2.09: Evitando la espera activa
Práctica 2.10: Exclusión mutua: soluciones hardware