laboratorio de programación concurrente: sincronización


Esta semana tenía que haber estado dedicada a la concurrencia en el lenguaje de programación Ada, pero la ausencia del profesor que nos lo iba a impartir la parte teórica de lunes a miércoles ha provocado que tengamos una extensión de la parte de laboratorio de programación concurrente en java. En este laboratorio estamos realizando una práctica tutorizada de simulación de trenes en la que ya hemos hecho la primera parte de conversión en Threads concurrentes de los objetos Tren.

Esta conversión es el primer paso del trabajo de asegurar que las clases compartidas por esos Threads, Tunel y CentroRegulación, sean thread-safe protegiendo su estado y asegurando que se cumplen los invariantes en cuanto a su correcta utilización por parte de los trenes (en la clase Tunel solamente pueden circular trenes en un sentido, pertenecientes al mismo tramo, en un momento dado)

Clase CentroRegulacion

En esta clase tenemos en primer lugar una variable privada numTrenes que representa en gran media el estado de la clase y que es compartida por los Threads a través de los métodos entro() y salgo(). Estos métodos deben ser sincronizados con el lock intrínseco de la clase (que posee como sabemos al derivar de la clase Object, como el resto de clases de Java) De esta manera un Thread de un objeto Tren deberá adquirir el lock, para llamar a cualquiera de los dos métodos para modificar la variable numTrenes, y liberarlo después. Así garantizamos el acceso exclusivo a dicha variable y que su valor será correcto tras cada interacción. Para activar el acceso exclusivo debemos poner synchronized en ambos métodos.

Clase Tunel

En el caso de esta clase, va a ser utilizada también por múltiples threads (además del thread del programa principal, todos los trenes) Por lo tanto hay que proteger el acceso al estado del túnel. En primer lugar tendremos que aplicar la exclusión mutua mediante el lock intrínseco del objeto (por derivar de la clase raíz Java Object) poniendo la clave synchronized en todos los métodos que toquen el estado de la clase Tunel (i.e: azulesEstacionados, rojosEstacionados, numCirculando y aurotizadosRojos) Estos métodos son: entro(), espero() y salgo(). Para el caso de la variable booleana autorizadosRojos empleamos una técnica para evitar que dicha variable se guarde en registros temporales y se obligue a leer directamente de memoria que es poniendo la clave volatile. La carga del dato en memoria y la escritura en memoria del dato será además atómica.

Otras cuestiones

El cálculo y asignación del tiempo para la próxima partida puede realizarse en mi opinión tanto cuando se entra como cuando se sale. Creo que lo más lógico es hacerlo cuando se entra, entiendo que dicho tiempo es para el tren (thread) que entra y no para todos, aunque intuyo que este puede ser el sentido.

Tenemos también en mi opinión una candidata a operación atómica que es el conjunto formado por entro() y estaciono(). No se puede interrumpir la ejecución conjunta de los dos métodos porque entonces podría darse el caso de que el tren incrementara la variable numTrenes pero no llegara a colocarse físicamente dentro del centro de regulación (en el interfaz gráfico con la llamada a situaCentroRelgulacion) Para solucionarlo creo que lo mejor es poner privado el método de estacionamiento y llamarlo desde el método (ya sincronizado) de entrada.

Además si se observa el código se puede ver que en salgo() se hace la misma llamada que en estaciono() pero esta vez para quitar el tren físicamente (gráficamente) del centro de regulación. Se puede considerar que estaciono() no tiene sentido como método y se puede eliminar y pasar la llamada a situaCentroRelgulacion directamente a la función entro().

Al intentar implementar los resultados del razonamiento de los tres párrafos anteriores, los razonamientos se han revelado incorrectos ya que al hacer los cambios descritos la simulación no se comporta de forma correcta. Los trenes de un color salen en bloque del centro de regulación y además se quedan pintados en el aparcamiento del túnel tras salir del mismo.

Estos artefactos (comportamientos incorrectos) se deben por una parte a que no había sincronizado la clase Tunel cuando hice las modificaciones (si lo hago desaparece el artefacto del aparcamiento del túnel) Por otra parte parece que una modificación de actualiza() en la clase Tren que he realizado, en el estado HACIA_EL_CENTRO_DE_REGULACION, en la que el tren solamente da una vuelta y le hago entrar en el centro de regulación, es un error.

Al volver a reponer al código anterior (echando para atrás todos los cambios) he visto que el método estaciono() es fundamental para programar el estado HACIA_EL_CENTRO_DE_REGULACION. El método tiene que existir y además ser independiente del método entro(). Así que lo mantengo como estaba (método público)

Además he descubierto que el hecho de que salgan todos los trenes en bloque del aparcamiento del túnel se debe a poner el cálculo del tiempo de próxima salida en el método entro(). Aunque está en exclusión mutua parece claro que la entrada de los threads está muy ajustada en su creación (acceden a ese método en el constructor) y los tiempos están muy cercanos. Esto me ha hecho pensar cuál es el efecto de ponerlo en salgo(). El cálculo solamente se realiza cuando un tren que está en el centro de regulación recibe el permiso de salida. La primera vez que se ejecuta esto es tras completar una vuelta el primer tren que ha salido.

6 comentarios en “laboratorio de programación concurrente: sincronización

  1. Otra de las especificaciones que tenemos en la implementación concurrente es que la función entro() de la clase Centroregulacion debe suspender el Thread del tren que lo invoca hasta que el centro de regulación autorice su salida, momento en el que el Thread será activado de nuevo. Para implementar esta especificación debemos hacer uso del mecanismo de comunicación de wait()/notify().

    No obstante, aquí la activación del objeto tren no depende de la llamada a notify() de otro objeto tren cuando sale del centro de regulación sino que existe un tiempo establecido por el centro de regulación para el cual se otorga el permiso de salida y por lo tanto determina cuándo se debería activar el objeto tren al que le toca salir, de los que están esperando en la cola de threads suspendidos en el centro de regulación. Por ello vamos a utilizar la versión de wait (tiempo) en la que especificamos un tiempo de suspensión al thread que lo invoca. De esta manera el objeto será despertado de forma automática transcurrido el tiempo estipulado y podrá comprobar si tiene autorización de salida, a lo que responderá con salgo() o por el contrario debe seguir esperando y pasar de nuevo a estar suspendido.

    Las cuestiones que se plantean ahora es dónde metemos el código del wait(tiempo) y qué cantidad de tiempo ponemos. En cuanto a la primera cuestión, los dos posibles lugares son entro() y salgo(). La especificación conduce a pensar que debería estar en el primer método. El problema es que dicho método se llama en el constructor del tren antes de poner en marcha el thread por lo que no se puede suspender en dicho método (todavía no está en marcha) Así que no hay más remedio que colocarlo en el segundo método. El tiempo será igual al tiempo de actualización que es de 100 milisegundos.


    public synchronized void salgo() {
    while (!permisoSalida()) {
    try {
    wait(100);
    } catch (InterruptedException e) {}
    }
    numTrenes=numTrenes-1;
    proximaPartida=(long) (System.currentTimeMillis() +
    (laRed.getTramoFerroviario(tramo).longitudTramoFerroviario())/
    (RedFerroviaria.numMaxTrenesPorTramo()-2)*100);
    laRed.situaCentroRegulacion(tramo, numTrenes);
    }

    Sin embargo esto no tiene mucho sentido porque el método salgo() solamente se llama cuando el thread del tren comprueba fehacientemente que tiene el permiso de salida (está especificado en su máquina de estados) ¿Cuál es la solución?

  2. Al final, he conseguido una solución al problema de la red ferroviaria pero no es totalmente correcta. Lo he editado y compilado con el eclipse 3.4.1. Los detalles de la implementación son los siguientes:

    – El thread Main crea los threads Tren (objetos activos) Tras dormir (sleep) el tiempo de simulación menos los últimos N segundos el tread Main interrumpe los threads Tren. Luego espera N segundos a la finalización de los threads (sleep)

    NOTA: El programa no obstante no se para de forma completa por el conocido motivo del sistema de gestión de la interfaz gráfica en la RedFerroviaria (basado en swing, que nos ha sido dado y no hemos tocado)

    – Los Thread Tren si inicializan, se meten en el centro de regulación y se inician (start), todo ello se hace en su constructor. En el método run() se hace un bucle condicionado a la no interrupción (usando isInterrupted()) en el que se ejecuta su máquina de estados cada cierto tiempo, marcado por la velocidad especificada en su construcción (sleep(1000/velocidad))

    > La nueva máquina de estados en concurrencia tiene: EN_CENTRO_REGULACION, HACIA_TUNEL, EN_TUNEL, HACIA_CENTRO_REGULACION. He eliminado EN_ACCESO_TUNEL por que he considerado que el mecanismo de sincronización implementado en la clase Tunel lo hace innecesario.

    – El primer objeto compartido es CentroRegulación. Aquí hay que proteger el estado (numTrenes) para lo cual se utiliza el lock intrínseco de Object, sincronizando los métodos entro(), salgo() y estaciono() (posiblemente este último no sea necesario) Luego hay que sincronizar los threads que lo usan (los trenes que entran y se estacionan) a la hora de “salir” de CentroRegulacion mediante el bloqueo en la lista de espera de Object haciendo uso del mecanismo wait() temporizado sujeto a la condición de obtener el permiso se salida (variable condicional) en el método salgo()

    > El permiso de salida depende del cálculo de la variable proximaPartida que realiza cada thread Tren que sale del CentroRegulacion (por lo que es una especie de señal/notificación diferida mediante una condición) una vez obtenido el permiso de salida.

    – El segundo objeto compartido es la clase Tunel. Aquí también hay que proteger el estado: abierto/cerrado, número de trenes circulando en su interior y número de trenes en los aparcamientos de entrada del tunel en cada uno de los sentidos. Para hacer esto se sincronizan en el lock intrínseco de Object los métodos: entro(), espero(), salgo() y estaAbierto() En segundo lugar sincronizamos los threads de los trenes de distinto tramo mediante el bloqueo (espera pasiva) de los threads Tren que no pueden ocupar el túnel porque está cerrado con el mecanismo wait() temporizado en el método entro()

    > En entro () lo primero que hago es comprobar si el tunel está abierto. Si e así el método prosigue normalmente. Si no es que tengo que esperar (espero()) y me bloqueo temporalmente con la condición de apertura del tramo (estaAbierto()) Este comportamiento he considerado que sustituye al estado EN_ACCESO_TUNEL del objeto Tren.

    El problema que me he encontrado es cuando la temporización del Thread Main es tal que la señal de interrupción (interrupt()) se envía cuando uno o varios threads están en estado waiting en el aparcamiento del túnel. En ese caso el flag de terminación no se pone a true y entonces los threads no terminan nunca.

  3. Ante la situación incómoda de la solución que encontré ayer he estado pensando y ensayando algunas ideas para ver si resolvían el problema.

    (1) Me di cuenta de que la situación se podía arreglar si encontraba la forma de mandar la señal de interrupción al thread que se había salido del wait() y estaba por tanto en la sección catch. Por ello introduje una llamada a interrupt() utilizando para ello la referencia estática al objeto de la clase Thread Thread.currentTread() que representa al thread que en ese momento está ejecutando el método del objeto compartido. El resultado de estas modificaciones sin embargo no tuvo el efecto esperado y provocaba que los trenes se pararan a la salida del túnel o se quedan bloqueados en el aparcamiento del túnel.

    (2) Intenté volver a tirar la interrupción desde la sección catch en forma de una interrupción RuntimeException. Pero otra vez sin éxito.

    También modifiqué el método run(), pero más como una cuestión de corrección estética que para solucionar el problema, cambiando la función sleep por un wait():


    public synchronized void run() {
    while (!isInterrupted()) {
    actualiza();
    try {
    wait(1000/velocidad);
    //Thread.sleep(1000/velocidad);
    } catch (InterruptedException e1) {
    terminado = true;
    }
    }
    }

    El hecho de que algunos threads (los que se estaban en ese momento esperando en el aparcamiento del túnel y también los que estaban dentro del centro de regulación) se perdieran la señal de interrupción lo confirmé colocando una instrucción de impresión de un mensaje en la consola con el nombre del thread en la sección catch (InterruptedException e) de los métodos bloqueantes. Hay al menos tres threads que se pierden la señal!

    Al final logro una solución poco elegante (desde mi punto de vista) de poner la variable privada booleana terminado como una variable estática lo que la convierte en una variable de clase compartida por todos los objetos de la clase y accesible a través de la clase (bien asignándola directamente si es pública o a través de un método estático si es privada) El efecto de este cambio es que basta con que uno de los objetos que representan a los threads reciba la señal para que provoque la asignación de dicha variable y valga para todos los objetos de la clase, que reciben de forma efectiva la señal. La falta de elegancia viene del hecho de que todos los objetos que reciban la señal de interrupción van a acabar escribiendo en la variable de forma redundante, gastando ciclos de reloj de forma inútil (la variable está protegida de las series de escrituras por su condición atómica de tipo básico y su condición de volátil)

  4. Al final he conseguido una solución elegante que funciona. He puesto la variable booleana terminado como privada y volátil y he puesto un método público termina() que pone la variable a true. Este método va a ser llamado desde el programa principal antes de mandar la señal de interrupción, de la siguiente manera:


    for (int i=0;
    i < redferroviaria.numMaxTrenesPorTramo();
    i++){
    listaTrenesAzules[i].termina();
    listaTrenesAzules[i].interrupt();
    listaTrenesRojos[i].termina();
    listaTrenesRojos[i].interrupt();
    }

    El método run() quedará también modificado, no procesando en su sección catch nada:


    public synchronized void run() {
    while (!isInterrupted()) {
    actualiza();
    try {
    wait(1000/velocidad);
    //Thread.sleep(1000/velocidad);
    }catch (InterruptedException e1) {}
    }
    }

    He pensado también que podría haber utilizado la función estática isalive() en el programa principal para comprobar si existía algún thread que seguía funcionando y volverle a mandar la señal de interrupción … pero eso tampoco es muy elegante!

    He mandado a los profesores la solución que he obtenido (el lunes obtuve la prórroga para poder seguir con la práctica esta semana, indicando que quería hacerlo bien y enterarme bien de los conceptos)

  5. Laura Barros, la profesora del laboratorio me ha contestado al correo electrónico que le envié con mi solución parcial y el problema que tenía con la parada de los threads. Yo la he contestado con mi solución final. Este es el correo:

    De: Laura Barros Bastante
    Para: Gerardo de Miguel
    Fecha: Hoy 12:24:24
    Adjuntos: RedFerroviariaSolucion.zip

    Buenos días Gerardo,

    Intentaré responderte a tus preguntas.

    Gerardo de Miguel Gonzalez escribió:

    > Buenos Días Laura,
    >
    > He conseguido una solución al problema de la red ferroviaria pero no es totalmente correcta. Te incluyo el zip con el proyecto eclipse (lo he editado y compilado con el elipse 3.4.1)

    >
    > Me gustaría que me comentaras la solución. Yo voy a seguir intentando pensar una solución hoy (me gustaría tenerlo resuelto hoy mismo)
    >
    > Los detalles de la implementación son los siguientes:
    >
    > * El thread Main crea los threads Tren (objetos activos) Tras dormir (sleep) el tiempo de simulación menos los últimos N segundos el tread Main interrumpe los threads Tren. Luego espera N segundos a la finalización de los threads (sleep)
    >
    > NOTA: El programa no obstante no se para por el conocido motivo del sistema
    > de gestión de la interfaz gráfica en la RedFerroviaria.

    OK

    >
    > * Los Thread Tren si inicializan, se meten en el centro de regulación y se inician (start) En el método run() se hace un bucle condicionado a la no interrupción (isInterrupted()) en el que se ejecuta su máquina de estados cada cierto tiempo, marcado por la velocidad especificada en su construcción (sleep(1000/velocidad))

    OK

    >
    > – La nueva máquina de estados en concurrencia tiene: EN_CENTRO_REGULACION, HACIA_TUNEL, EN_TUNEL, HACIA_CENTRO_REGULACION. He eliminado EN_ACCESO_TUNEL por que he considerado que el mecanismo de sincronización implementado en Tunel lo hace innecesario (tal vez esté equivocado)

    Como el entro() del túnel llama al método estaAbierto(), creo que tienes razón y no es necesario. Además parece que funciona :)

    >
    > * El primer objeto compartido es CentroRegulación. Aquí hay que proteger el estado (numTrenes) para lo cual se utiliza el lock intrínseco de Object, sincronizando los métodos entro(), salgo() y estaciono() (posiblemente este último no sea necesario, es así?) Luego hay que sincronizar los threads que lo usan (los trenes que entran y se estacionan) a la hora se “salir” de CentroRegulacion mediante el bloqueo en la lista de espera de Object haciendo uso del mecanismo wait() temporizado sujeto a la condición de obtener el permiso se salida (variable condicional) en el método salgo()

    En estaciono() si lo llamaras dentro del método entro() que es syncronized, no haría falta que lo pusieras synchronized, pero tal y como lo tienes, que lo llamas desde la máquina de estados está bien ponerlo. El wait() temporizado está bien por lo que hablamos de que el notify en el salgo() aquí no interesa.

    >
    > – El permiso de salida depende del cálculo de la variable proximaPartida que realiza cada thread Tren que sale del CentroRegulacion (por lo que es una especie de señal/notificación diferida mediante una condición) una vez obtenido el permiso de salida.

    OK

    >
    > * El segundo objeto compartido es el Tunel. Aquí también hay que proteger el estado (abierto/cerrado, número de trenes circulando en su interior y número de trenes en los aparcamientos de entrada del tunel en cada uno de los sentidos) para lo cual se sincronizan en el lock intrínseco de Object los métodos: entro(), espero(), salgo() y estaAbierto() n segundo lugar sincronizamos los threads de los trenes de distinto tramo mediante el bloqueo (espera pasiva) de los threads Tren que no pueden ocupar el tunel porque está cerrado con el mecanismo wait() temporizado en el método entro()

    En este caso, si sería interesante que cuando los trenes hagan salgo() hicieran notify() sobre los threads que estén esperando en el wait() de entro() en vez de la opción del wait temporizado.

    >
    > – En entro () lo primero que hago es comprobar si el tunel está abierto. Si es así el método prosigue normalmente. Si no es que tengo que esperar (espero()) y me bloqueo temporalmente con la condición de apertura del tramo (estaAbierto()) Este comportamiento he considerado que sustituye al estado EN_ACCESO_TUNEL del objeto Tren.

    >
    > * PROBLEMA: el programa funciona correctamente excepto en el caso en el que la temporización del Thread Main es tal que la señal de interrupción (interrupt()) se envía cuando uno o varios threads están en estado waiting en el aparcamiento del tunel. En ese caso el flag de terminación no se pone y entonces los threads no terminan nunca. En cuanto llegan al centro de regulación vuelven a salir otra vez como si nada hubiera pasado.

    Creo que es porque cuando hacemos interrupt() sobre los trenes, si estás en wait(), salta la excepción y no las tienes tratadas.


    try {
    wait(100);
    } catch (InterruptedException e) {
    //Hay que tratar la excepción por si interrumpen
    }

    >
    > PD: Si me puedes mandar al final de la jornada la solución que le disteis vosotros para cotejar con la mía te lo agradecería enormemente ….

    Te mando la solución en adjunto (compilado con eclipse 3.2, pero funciona también en 3.3 y espero que en 3.4, lo único que tendrás que borrar la librería swt y poner la tuya). Si tienes más dudas pásate por aquí y las comentamos, es más rápido :)

    Un saludo,

    NOTA: He probado la sugerencia de hacer un <codewait () sin temporización en entro() y un notify en salgo() y funciona. La cuestión es que de esa manera solamente un tren de un color circula por el túnel en cada momento con la temporización que tengo (en mi caso se acumulan los cinco azules en el apartadero del túnel y luego van saliendo uno a uno del mismo según sale uno del túnel)

    NOTA: He probado con temporización 10/8 con las dos soluciones i.e wait temporizado en entro() y wait/notify en entro/salgo() y la segunda es más suave y “realista”.

  6. Esta es la respuesta que he dado al correo de Laura con mi visión de la solución final (todavía no he podido acceder a la solución que me mandó ella ….)

    Buenas Tardes Laura,

    Si es cierto que sería más rápido ir allá :) Pero no he tenido tiempo para pasarme.

    Sobre la cuestión de las excepciones sin tratar es correcto. Me di cuenta casi en seguida que los threads que no se paraban eran porque recibían la señal de interrupción (InterruptedException) cuando estaban en la lista de espera de Object, tanto en los aparcamientos del tunel como dentro de los centros de regulación. La señal era capturada en el catch del InterruptedException. El problema era y es que no encontraba la manera de rellenarla para “propagar” esa señal a los threads originales para que la procesaran. El wait() se realiza sobre las condiciones de salida de tunel abierto (Tunel) y permiso de salida (CentroRegulacion) y no se sale de ahí hasta que no se cumplen. Además no conseguía la manera de obtener una referencia a los objetos activos para indicar la terminación. Si se puede obtener una referencia a los threads de los objetos activos mediante sus métodos estáticos pero no había ninguno que fuera de utilidad para señalar la terminación.

    Se me ocurrió poner el flag de terminación de los threads de los trenes (variable booleana terminado) como una variable estática volátil (asegurando operación atómica por ser un tipo básico y evitando las optimizaciones del compilador obligando a leer el dato de memoria) De esta manera el primer thread que cogiera la señal interrupt() podría la variable de clase a true, la cual la verían todos los threads. El programa funcionó. El problema de esta solución es que todos los threads activos que no estuvieran en estado waiting escribirían en la variable un valor redundante.

    La segunda fue rehabilitar el método termina() de Tren que pone a true la variable booleana de terminación y llamarla desde el programa principal a la par que la de interrupción (sin ponerlo synchronized pero manteniendo la condicón volatil de la variable booleana private):


    [...]
    for (int i=0;
    i < redferroviaria.numMaxTrenesPorTramo();
    i++){
    listaTrenesAzules[i].termina();
    listaTrenesAzules[i].interrupt();
    listaTrenesRojos[i].termina();
    listaTrenesRojos[i].interrupt();
    }
    [...]

    También cambié la función run() sustituyendo el sleep por un wait() remporizado (este cambio no es necesario pero me pareció más elegante) y eliminar la asignación a true de “terminado” en el tratamiento de la excepción:


    public synchronized void run() {
    while (!isInterrupted()) {
    actualiza();
    try {
    wait(1000/velocidad);
    //Thread.sleep(1000/velocidad);
    }catch (InterruptedException e1) {}
    }
    }

    NOTA: Tuve que poner synchronized el método run ya que sino me daba una excepción muy rara …

    De esta manera ha funcionado perfectamente.

    Respecto a la puesta como synchronized del método espero() de la clase Tunel creo que debo quitarlo porque solamente lo utilizo en entro() de la misma clase Pero creo que además debo ponerlo private para proteger las variables rojosEstacionados y azulesEstacionados., asegurando el acceso desde un método synchronized (entro())

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s