Código típico: try { Thread.sleep(1000); } catch(InterruptedException ex) {}
Ese código puede ser inofensivo en un programa trivial de prueba, pero rara vez se debe utilizar en casos reales. Por qué? porque esto destruye por completo el mecanismo convencional (y único) de interrupción de Threads.
Por qué es importante esto?
Porque (por muchos motivos) los “threads” pueden necesitar ser interrumpidos por el programa (por ejemplo, cuando éste va a terminar.)
En aplicaciones stand-alone normalmente es posible aplicar el conocido System.exit() si se desea terminar el programa (y todo lo que se ejecute en el JVM); sin embargo, esto no siempre será posible debido a que:
* A veces no se desea terminar el programa, sino sólo un thread que realiza cierta actividad especializada
* A veces se desea terminar “nuestro programa”, pero el JVM podría estar ejecutando otras aplicaciones (por ejemplo, en el caso de un contenedor JEE)
A fin de interrumpir (o forzar la finalización de) la ejecución de un thread hay básicamente dos mecanismos:
1) Que el thread constantemente verifique el valor de una variable “de control”; cierto valor significará que el thread debe concluir, y éste simplemente retornará de su método inicial (por ejemplo, retornará del run() de Runnable.)
2) Que el thread sea “interrumpido” (generalmente) desde otro thread (mediante Thread.interrupt().)
El primer mecanismo se suele ilustrar en los libros mediante una variable tal como:
volatile boolean estado_shutdown = false;
la cual es consultada con cierta regularidad por los threads que deben terminar en algún momento:
public void run() {
for(;;) {
if(estado_shutdown) {
return; // fin del thread
}
// hacer algo
}
}
Este mecanismo suele ser aplicable sólo si tenemos a nuestra disposición el código fuente que ejecuta cada thread; asimismo, no siempre es sencillo lograr que la lectura de la variable de estado se haga con aceptable frecuencia: por ejemplo, el thread podría estar esperando una línea desde el teclado y la variable sólo sería leída después de que el usuario complete su texto (lo cual podría no ocurrir jamás.)
El segundo mecanismo tiende a ser más general: basta con mantener una referencia a cada thread en ejecución, y mediante su método interrupt(), el thread se interrumpe… en teoría!
Cómo es un thread interrumpible?
Para bien o para mal, el método interrupt() en sí no interrumpe nada. Lo único que hace es cambiar un flag de estado del thread a “interrupmido”. En principio, esto obligaría a que el programador del thread escriba algo como:
for(;;) {
if(Thread.currentThread().isInterrupted()) {
return; // debe salir del loop y finalizar el thread
}
// sigue mas codigo
}
lo cual se parece mucho al mecanismo anterior! Esta técnica se utiliza en ciertos casos, pero por lo general es más fácil aprovechar una característica de ciertos métodos de la librería Java, a saber, el lanzamiento de InterruptedException cuando detectan el estado “interrupmido” del thread. Por ejemplo, es frecuente que los loops tengan una pausa para reducir el consumo de CPU:
for(;;) {
Thread.sleep(1000);
// sigue mas codigo
}
La llamada Thread.sleep() tiene como una de sus características el lanzar InterruptedException cuando detecta el estado “interrumpido”, lo cual la hace casi equivalente al código anterior.
Otras llamadas potencialmente “lentas” (por ejemplo, la lectura de una BlockingQueue) también tienen este comportamiento.
Para que este mecanismo sea útil, es necesario que las funciones que utilizan estos métodos declaren “throws InterruptedException”, y jamás de “tragarse” la excepción con un “catch(InterruptedException e){}”.
Es muy importante comprender que el lenguaje Java no obliga a nada de esto (quizá debe considerarse un error de diseño del lenguaje), por lo que este mecanismo de interrupción requiere de la colaboración de programador.
Ejemplo:
public void run() {
try {
hacer_un_trabajo_que_toma_mucho_tiempo();
} catch(InterruptedException e) {
System.out.println(“Interrumpido. Termina thread”);
}
}
void hacer_un_trabajo_que_toma_mucho_tiempo() throws InterruptedException {
for(;;) {
// hacer algo aqui
Thread.sleep(100);
}
}
En el ejemplo anterior, si hubieramos “tragado” la InterruptedExceptiton de Thread.sleep(), el thread no podría ser interrumpido exteriormente.
Nótese que hemos capturado la excepción en run(); esto es admisible debido a que sabemos de antemano que al retornar run() el thread va a terminar definitivamente. En tal sentido, run() es un caso excepcional.
Otro detalle poco visible pero muy importante es que tras la captura de InterruptedException el thread vuelve a estado “no interrumpido”, lo cual es conveniente si hay más código que ejecutar a fin de que no se vuelva a disparar la InterruptedException por el mismo mecanismo.
Horror: I/O no lanza InterruptedException!
A estas alturas esto también puede ser considerado un error de diseño: la librería Java de I/O (Streams, Readers, Writers) tienen métodos “lentos” que no generan InterruptedException. El caso típico es:
System.in.read();
Si el thread que ejecuta esta sentencia se interrumpe con Thread.interrupt() no ocurre absolutamente nada! Esto se puede deducir del hecho que estas clases en general no lanzan InterruptedException. Este problema se extiende a todo lo referido a I/O (por ejemplo, los sockets de red.)
Hay varias maneras de resolver esto dependiendo de la circunstancia concreta. Por ejemplo, los tutoriales de programación de redes con Java sugieren que la lectura/escritura de los sockets se interrumpa indirectamente mediante el cierre del socket (lo cual genera un IOException.)
Una solución más general pero por lo general más compleja consiste en utilizar Java NIO, la cual proporciona nuevas clases y métodos que sí responden a la interrupción del thread.
Un caso frecuente se da al tratar de interrumpir BufferedReader.readLine() (por ejemplo, usado para leer desde System.in.) La técnica antes descrita (cierre del Reader) no funciona debido a que close() es sincronizado a la vez que el readLine (por lo que se suspende hasta que éste último concluya.) Una solución conocida (pero poco estética) consiste en este código:
while (!reader.ready()) {
Thread.sleep(100);
}
String line = reader.readLine();
El método ready() garantiza que el readLine() retornará de inmediato. El sleep() nos proporciona el disparo de InterruptedException.
Para terminar, a modo de recapitulación, consideremos la escritura de un método interrumpible que invoca repetidamente a otro método (que no hemos confeccionado nosotros). Algo como:
void nuestroMetodo() {
for(;;) {
x.otroMetodo();
}
}
Hay dos casos: si otroMetodo() declara “throws InterruptedException” probablemente basta con agregar esto en nuestro método:
void nuestroMetodo() throws InterruptedException {
for(;;) {
x.otroMetodo();
}
}
De no ser así, tenemos al menos dos opciones:
void nuestroMetodoOpcion1() throws InterruptedException {
for(;;) {
x.otroMetodo();
Thread.sleep(nnn);
}
}
y:
void nuestroMetodoOpcion2() throws InterruptedException {
for(;;) {
x.otroMetodo();
if(Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
}
}
nuestroMetodoOpcion1() es conveniente si deseamos asegurar que el thread no monopolice el uso de CPU; nuestroMetodoOpcion2() es conveniente si deseamos que “otroMetodo()” esté (casi) siempre en ejecución (por ejemplo, si debe atender un evento externo y deseamos brindar una respuesta inmediata.
Recordando lo que se indicó mucho más arriba, nótar que si “otroMetodo()” captura y “se traga” InterruptedException (lo cual borra el flag de “interrumpido” en el thread), entonces ninguna de nuestras opciones tendría posibilidad de detectar la interrupción.
Conclusión
Cada método de la librería que indica “throws InterruptedException” es nuestro amigo. Jamás ignoremos su InterruptedException en aras de una aparente simplificación en nuestros métodos.
0 Responses
Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.