G.Bordel >Docencia >TAP Técnicas Actuales de Programación (curso 2010-2011)
desprotegido Intro. desprotegido Temario desprotegido Calendario desprotegido RPF desprotegido Recursos protegido Práctica protegido Gest. Alum.
tema_anterior Tema 10: Hilos tema_siguiente
  1. Introducción.
  2. Ciclo de vida de un hilo.
  3. Distribución de la CPU. Prioridades. El problema de la "inanición".
  4. Mecanismos de sincronización de hilos. El problema del interbloqueo.
  5. Agrupamientos de hilos.
  6. Estudio de un ejemplo de programación con hilos.[ejercicios]

10.4- Mecanismos de sincronización de hilos. El problema del interbloqueo

Palabras reservadas en Java
abstractbooleanbreakbytecasecatch
charclassconst*continuedefaultdo
doubleelseextendsfinalfinallyfloat
forgoto*ifimplementsimportinstanceof
intinterfacelongnativenewpackage
privateprotectedpublicreturnshortstatic
strictfp**superswitchsynchronizedthisthrow
throwstransienttryvoidvolatilewhile

En este apartado veremos el mecanismo básico de sincronización entre hilos. Dos hilos que colaboran en una determinada tarea comparten información y por tanto deben mantener entre ellos algún tipo de sincronización que les permita "entenderse". El mecanísmo básico de colaboración es el acceso a estructuras de datos comunes, y en este caso el problema principal a solventar es el de la "exclusión mutua", es decir la posibilidad de que el acceso por parte de un hilo a una estructura impida el acceso a otro hilo mientras la acción del primero no haya terminado. El modo de llevar esto a cabo consiste en la utilización de una variable que indique cuándo el recurso esta libre y cuándo esta siendo utilizado. A esta variable suele llamarsele de distintos modos: semáforo, monitor, cerrojo, bloqueo; y aunque pueda haber algunas variantes de esta técnica, nos centraremos en el caso más claro: el semáforo binario (en Java de habla de "monitores" -lock-).

Un semáforo binario es una variable binaria que nos indica simplemente si un determinado recurso esta tomado en propiedad por un hilo. Por tanto, cuando un hilo necesita acceder al recurso debe consultar el semáforo y en caso de que éste indique que el recurso es propiedad de otro hilo debe quedarse a la espera de que se libere. En caso contrario debe marcar en el semáforo que toma la propiedad y proceder a utilizarlo.

Este procedimiento -que puede verse en la figura- presenta un problema que es necesario evitar: la acción de consultar el estado del semáforo y de marcar en él la toma de posesión del recurso han de ser indivisibles (no puede perderse el procesador entre ellas) porque de lo contrario podria llegarse a una inconsistencia como veremos a continuación. A una sección como esta, que no puede ser interrumpida, se le denomina sección crítica. Diremos por tanto que es preciso disponer de un mecanismo de implementación de secciones críticas para poder implementar a su vez semáforos binarios.


.

La inconsistencia que puede producirse en caso de no ejecutar la comprobación del estado del semáforo y su activación se produce con los siguientes sucesos:

  • El hilo A necesita el recurso y consulta el semáforo viendo que esta libre.
  • El hilo A pierde la CPU y posteriormente es asignada al hilo B.
  • El hilo B necesita el recurso y consulta el semáforo viendo que esta libre.
  • El hilo B marca "ocupado" en el semáforo y comienza a trabajar con el recurso.
  • El hilo B pierde la CPU (sin acabar de trabajar con el recurso) y posteriormente es asignada al hilo A.
  • El hilo B marca "ocupado" en el semáforo y comienza a trabajar con el recurso.
  • El hilo A marca "ocupado" en el semáforo (la consulta la hizo antes de perder la CPU) y comienza a trabajar con el recurso, de modo que puede interferir con el trabajo de B. Ambos hilos han accedido "simultáneamente" al recurso.

Java dispone de un mecanísmo para adquirir la "propiedad" de los objetos (todo objeto dispone de un "monitor" -o semáforo-). Esto hace innecesaria la especificación de secciones críticas ya que se implementan automáticamente en el mecanísmo de los "monitores".

La sincronización se hace por tanto mediante el bloqueo de objetos, es decir, mediante la reserva de exclusividad de acceso a un objeto por parte de un solo hilo. Esto puede hacerse de dos modos: por una parte, en el mismo objeto declarando métodos como "sincronizados" su código se ejecuta con la posesión del objeto.

1-   public synchronized int get() {
2-       ... //sentencias ejecutables sin cesión de CPU.
3-   }

Por otro lado, en todo momento puede definirse un bloque (sentencias entre llaves) precedido por la palabra synchronized y el identificador del objeto a bloquear entre paréntesis.

1-   ...
2-   synchronized (X) {
3-       ... //sentencias ejecutadas con propiedad sobre el objeto X...
4-   }
5-   ...

Finalmente, antes de acabar este apartado, insistiremos en que, si bien este mecanismo es fácil de comprender, el problema de la colaboración entre hilos para el que se proporciona no es trivial. Un problema concreto relacionado con el acceso en exclusión que hemos estudiado aquí es el "Interbloqueo" (Deadlock). Se trata de un problema extremadamente grave por el cual dos o más procesos quedan bloqueados es estado de espera sin posibilidad de progresar por tratarse de una espera "circular". Veamos el caso con dos hilos:

  • El hilo A toma en exlusión el recurso R1.
  • El hilo A pierde la CPU antes de liberar R1.
  • El hilo B toma en exlusión el recurso R2.
  • El hilo B pierde la CPU antes de liberar R2.
  • El hilo A pretende actuar sobre el recurso R2 pero al ser propiedad de B pierde la CPU pasando a espera.
  • El hilo B pretende actuar sobre el recurso R1 pero al ser propiedad de A pierde la CPU pasando a espera.
  • Los dos últimos pasos se repiten sin fin.

Como puede verse este problema da lugar al bloqueo de la aplicación sin tratarse de un error de codificación, ni siquiera un error previsible desde el diseño de cada hilo, sino de es producto de la interacción entre todos ellos. Una vez que el problema se ha dado no existe una solución y todo lo que se puede hacer es abortar la ejecución de la aplicación o al menos uno de los hilos bloqueados para que los otros progresen y se produzca el error que resulte de la desaparición de un hilo. En caso de que el total de la aplicación este en nuestras manos, en ocasiones podremos establecer mecanismos de evitación del interbloqueo (p.ej. estableciendo un orden en los recursos y obligando a todo hilo que bloquee todos los recursos que vaya a necesitar siguiendo el orden establecido, con lo que se evita toda circularidad en las esperas). Pero no siempre es posible establecer un inventario de recursos y por tanto un orden o bien no se dispone de control sobre todos los procesos para imponer el orden de acceso (esto les sucede a los sistemas operativos). En tales casos estamos expuestos a que se produzca el interbloqueo y sólo cabe establecer estrategias de cierto nivel de previsión y en todo caso de detección y aborto de procesos (como hacen los sistemas operativos).

Siguiente punto: 10.5- Agrupamientos de hilos


Plataforma de soporte a curso y contenidos (c) German Bordel 2005.