JAVA
Concurrencia y
sincronización de threads (II)
A lo largo de este artículo
vamos a tratar sobre cómo los threads pueden compartir datos de
una forma correcta, sin problemas, utilizando los diferentes métodos
disponibles para ello. Además se detallarán los modos
para la comunicación e intercambio de datos entre éstos.
La utilización de threads que
comparten datos puede dar a lugar a situaciones anómalas cuya
solución exige el uso de un mecanismo de sincronización
de acceso al recurso compartido. El lenguaje Java ofrece unos
sencillos mecanismos para solucionar este problema, por lo que la
solución suele resultar por regla general bastante simple y
efectiva.
SINCRONIZACIÓN Y
COMUNICACIÓN entre threads
Estos mecanismos se basan en el
concepto de "monitores", un esquema ampliamente utilizado y
que fue desarrollado por C.A.R. Hoare. No es necesario tener una
comprensión exacta de este mecanismo, pero es de gran ayuda
para conseguir un entendimiento de los problemas que se abordarán.
Un monitor es esencialmente un
cerrojo, que se aplica a un determinado recurso que es compartido, de
forma que a este recurso pueden acceder múltiples threads, pero
únicamente por uno de ellos a la vez.
El mecanismo de cerrojos se puede
entender con un ejemplo de la realidad. Supongamos que tenemos un
hotel con una única habitación libre. Cuando la ocupe un
inquilino, a este inquilino se le da una llave que le permite
disfrutar de esa habitación, pero en cuanto se marcha y
devuelve la llave, esa habitación esta disponible para otros
posibles inquilinos, de manera que la posesión de la llave
determina quién esta usando el recurso, o sea, la habitación.
El problema clásico que
determina el uso de cerrojos es el siguiente. Supongamos que dos
thread se alternan en el incremento del contenido, de una variable común,
un contador. En un determinado momento, el thread A lee el contenido
del contador, lo incrementa en uno y va a guardar en la variable el
nuevo contenido. Se puede dar el caso de que en ese momento el sistema
pase al thread de activo a dormido (se le acabó su tiempo), y
no ha podido guardar el valor correcto del contador.
Ahora le toca el turno al thread B,
que verá el mismo contenido que el thread anterior, debido a
que éste no le dio tiempo a guardar el resultado correcto. A
continuación thread el actual incrementará el contador y
guardará su resultado. Ahora se le acaba el tiempo, el sistema
ejecuta el thread A que en vez de incrementar el valor según lo
dejó el thread B escribirá su valor calculado
anteriormente y continuará donde terminó su ejecución,
escribiendo su propio resultado, por lo que tenemos que los dos thread
en vez de incrementar el contenido de una variable en dos unidades lo
han hecho en uno.
El uso de recursos compartidos
usando threads puede dar lugar a situaciones anómalas
Afortunadamente Java se encarga de
todo el proceso de sincronización, ejecuta internamente todo el
mecanismo de acceso, manipula el estado del cerrojo, o sea, su bloqueo
y correcto desbloqueo, por lo que las aplicaciones sólo tienen
que indicar los recursos que se van a bloquear.
LA PALABRA CLAVE: SYNCHRONIZED
Por regla general se suele necesitar
sincronización entre threads cuando queremos acceder
ordenadamente a un recurso compartido, normalmente un objeto. En otras
palabras, la sincronización se encarga de asegurar que un
thread en un determinado momento pueda manipular un objeto. En Java,
cada objeto tiene un cerrojo asociado con él. Para ser más
específicos, cada clase y cada instancia de la clase tiene su
propio cerrojo. Gracias a ello, se puede utilizar la palabra clave
synchronized que se utiliza para definir qué parte de código
(en realidad un método completo) puede ser accedido por un único
thread, por lo que sé encarga de gestionar el mecanismo de
cerrojos.
UN EJEMPLO: EL TOTAL BANCARIO
Desarrollemos un ejemplo que ponga
en práctica la teoría. Supongamos un banco que consta de
dos cajeros que se encargan de contar billetes de un valor cualquiera
en pesetas. Cada uno de ellos tiene que contar una cantidad de
billetes y cuando ha finalizado su tarea muestra el número
total de billetes que ha contado y la suma correspondiente. Si hacemos
que los dos cajeros sean dos threads y que cuenten el número de
billetes y den la suma de los que han contado, podremos poner en práctica
la sincronización.
En el primer caso vamos a hacer que
cada cajero cuente su propio montón de billetes, para mostrar
los cambios y cómo es posible el cambio de los resultados: import java.io.*;
import java.lang.*;
class Banco {
public static void main ( String args[])
{
try {
// Declaramos los dos montones
de billetes
Contador co1 = new Contador ();
Contador co2 = new Contador ();
// Declaramos los dos cajeros
Cajero c1 = new Cajero(co1);
Cajero c2 = new Cajero(co2);
// Se ponen a contar..
c1.start();
c2.start();
c1.join();
c2.join();
}
catch ( Exception e ){
e.printStackTrace();
}
}
}
La clase Contador se encargará
de contar los billetes y almacenar la suma y el total de billetes
contados. En este caso concreto se supondrá que el montón
es de billetes de 2.000 pesetas. class Contador {
int numBilletes = 0 ;
long suma = 0 ;
final int TOTAL_BILLETES = 10000 ;
final int VALOR_BILLETES = 2000 ;
void cuenta () {
// A contar la suma de los billetes
for ( numBilletes =0 ; numBilletes
< TOTAL_BILLETES; numBilletes ++ )
{
suma += VALOR_BILLETES ;
// Billetes de 2000 pts
Thread.yield();
}
System.out.println
( numBilletes+ " suman : "+
suma + " pts");
}
}
En la siguiente clase declaramos los
thread que serán los cajeros que cuentan. Cada cajero recibe un
montón para contar, o sea, un objeto de tipo Contador: class Cajero extends Thread {
Contador contadorCajero ;
Cajero ( Contador paramContador ) {
contadorCajero = paramContador ;
}
public void run () {
contadorCajero.cuenta();
}
}
Se obtiene lo siguiente:
10000 suman: 20000000 pts
10000 suman: 20000000 pts
Es un resultado correcto, ya que
cada thread tenía un montón de billetes diferentes para
contar.
Ahora supongamos que los dos cajeros
deben contar del mismo montón, o sea, lo comparten y por tanto,
la suma de lo que haya contado cada uno debe ser el resultado total.
Para ello, modificaremos el código
añadiendo lo siguiente // Declaramos los dos montones de billetes
Contador co1 = new Contador ();
// Ahora sobra, Contador co2 = new Contador ();
// Declaramos los dos cajeros y el mismo montón.
Cajero c1 = new Cajero(co1);
Cajero c2 = new Cajero(co1);
Con este cambio obtenemos:
10000 suman: 20002000 pts
10001 suman: 20002000 pts
Observando el resultado, comprobamos
que los dos cajeros no saben contar y además, un cajero ha
contado un billete más que el otro, y encima el total sumado es
el mismo en ambos casos, por lo que está claro que ha producido
un error de sincronismo. En un momento un thread ha interrumpido a
otro en una parte crítica, en la suma o en la cuenta de los
billetes.
Un cerrojo es un mecanismo que
aplicado sobre un recurso permite un acceso correcto a éste por
parte de los threads
Por tanto, debemos utilizar un
mecanismo de sincronización que garantice que cuando un cajero
cuente un billete y lo sume, el otro no pueda intentar coger el mismo
billete y sumarlo. La solución que ofrece Java para resolver
este problema es de lo más simple y eficiente, utilizando la cláusula
synchronized en la declaración del método donde se
realiza la tarea "crítica".
Por tanto, cambiaremos el método
void cuenta() por: synchronized void cuenta ()
Si realizamos ese cambio, obtenemos
el siguiente resultado:
10000 suman : 20000000 pts
10000 suman : 40000000 pts
Es el resultado esperado. Una vez
que el primer cajero entra, no acaba hasta que termina de contar,
debido a que mientras no finalice su trabajo no ha salido del método,
impidiendo que el otro cajero pudiera tocar el montón de
billetes para contar, porque hemos declarado el método cuenta()
con la cláusula synchronized.
De todos modos, hay un detalle que
se tiene que explicar. ¿Por qué el cajero que finaliza
cuenta 40000000 en vez de 20000000?. Esto ocurre porque no se
inicializa la variable suma antes del bucle que cuenta los billetes,
por lo que el segundo cajero continúa la suma en donde la dejó
el anterior.
Si modificamos el código e
incluimos la inicialización, tendremos: void cuenta () {
// Cada cajero cuenta lo suyo
suma = 0 ;
// A contar la suma de los billetes
for ( numBilletes =0 ; numBilletes
< TOTAL_BILLETES ;numBilletes ++ ) {
suma += VALOR_BILLETES ;
Thread.yield();
}
A partir de este momento obtenemos
el siguiente resultado esperado tal y como detallamos:
10000 suman : 20000000 pts
10000 suman : 20000000 pts
Otra forma de realizar la
sincronización consiste en declarar el objeto compartido como
sincronizado en vez del método que lo contiene. Se realiza
entonces el siguiente cambio: public void run(){
contadorCajero.cuenta();
}
por: public void run(){
synchronized (contadorCajero ) {
contadorCajero.cuenta();
}
}
Se ejecuta esta nueva modificación
y se obtiene el mismo resultado. En este caso lo que se está
sincronizando es el acceso al objeto, no el método que lo
contiene.
El uso de cerrojos permite la
comunicación entre threads de forma que uno puede bloquear a
otro debido a diferentes causas, como una espera, un cálculo,
etc
La cuestión está en
que si un método es de tipo synchronized y si se ejecuta sobre
un objeto en un thread, otro thread no puede llamar a este método
sobre ese objeto, pero puede llamar a otro método sobre otro
objeto.
Recordando el caso en que el método
estaba declarado con synchronized, esto nos garantizaba que dos thread
no podían ejecutar a la vez ninguna parte del método,
pero sobre el mismo objeto, no sobre diferentes objetos. Si declaramos
dos objetos diferentes podríamos tener los mismos problemas.
Además, la cláusula
synchronized permite ejecutar dos métodos diferentes sobre el
mismo objeto o el mismo método sobre dos objetos diferentes,
pero impide que más de un thread ejecute la misma porción
de código al mismo tiempo sobre el mismo objeto.
La cláusula synchronized
permite sincronizar el acceso a un determinado método
Llegados a este punto debemos de
destacar que el hecho de usar synchronized de una forma poco práctica
puede conducirnos a interbloqueos y provoca una degradación del
rendimiento del código, cerca de cuatro veces, por lo que es
preferible utilizar esta palabra clave sólo para métodos
cortos y en aquellos que se consideran como imprescindibles.
UN POCO MÁS ALLÁ.
LOS MÉTODOS WAIT() Y NOTIFY()
Si bien mediante la utilización
de la cláusula synchronized podemos tener un acceso
sincronizado a un método o un objeto de un bloque de código,
el uso de wait() y notify() permite ampliar esta posibilidad. Estos métodos
pertenecen de por sí a la clase Object. Cada objeto en Java es
una subclase de Object, por lo que gracias a la herencia podemos
utilizar sus métodos. Usando wait() y notify() un thread puede
detenerse sobre un cerrojo en un punto cualquiera que podemos definir,
y entonces puede esperar a que otro thread le notifique que puede
continuar. Todo esto ocurre dentro de bloques sincronizados y se
cumple que un único thread puede ejecutarse a la vez. La
ventaja de este método es que ofrece mayor flexibilidad de
bloqueo y una forma de controlar los thread de manera más práctica.
La ejecución del método
wait() en un bloque sincronizado determinado permite que el thread
quede en un modo de "espera o bloqueado" sobre un
determinado cerrojo y por tanto pasa al estado de "dormido".
Un thread puede necesitar ser bloqueado porque tiene que esperar a que
otro thread haya terminado de realizar una tarea en otra parte del
programa, o se cumpla una determinada condición, etc. Entonces,
cuando se cumpla dicha condición o la tarea que espera se haya
realizado, el thread que estaba en "espera" por el bloqueo
es "desbloqueado" mediante una llamada al método
notify() por el thread que estaba activo, y el que estaba dormido
pasará a estado de activo, tomando el control sobre el cerrojo
correspondiente.
Cuando el thread que quedó
bloqueado toma el control sobre el cerrojo prosigue con la siguiente
sentencia donde se quedó, esto es, la siguiente sentencia tras
wait(). De todos modos, hay que tener en cuenta que esto depende de
cuando el thread que libera el cerrojo por el uso del método
notity() ya que si no se realiza la llamada el thread bloqueado no
continuaría ejecutándose. Por ello, existe una versión
sobrecargada del método wait() que permite especificar un
determinado tiempo de espera, a partir del cual el thread que está
bloqueado despierta. Tiene la ventaja de que se evitan bloqueos extraños
pero no garantiza un uso correcto de los cerrojos.
Vamos a desarrollar un ejemplo
sencillo en el que se utilicen wait y notify para poder apreciar la
utilidad de estos métodos. Supongamos que tenemos unos alumnos
de una clase esperando a que el profesor les comunique sus notas. Los
dos threads de Alumnos y el thread Profesor compartirán el
objeto Nota: import java.io.* ;
class escuela {
public static void main ( String args[] ){
try {
// Generamos el objeto Nota, el profesor
// y los dos alumnos
Nota laNota = new Nota ();
Profesor p = new Profesor ( laNota );
Alumno a = new Alumno ( "Javi", laNota);
Alumno b = new Alumno ( "Jose", laNota );
// Empezamos la ejecución
a.start();
b.start();
p.start();
a.join();
b.join();
p.join();
}
catch ( Exception e ){
System.out.println ( e.toString() );
}
}
}
class Alumno extends Thread{
Nota na ; // nota del alumno
String nom ; // nombre
Alumno ( String nombre , Nota n ) {
na = n ;
nom = nombre ;
}
public void run () {
System.out.println ( nom + " Esperado su nota" );
na.esperar(); // el alumno espera la nota
System.out.println ( nom + " recibio su nota"); }
}
class Profesor extends Thread{
Nota na ;
Profesor ( Nota n ){
na = n ;
}
public void run () {
System.out.println ( " Voy a poner la nota ");
na.dar (); // el profesor pone la nota del alumno
}
}
class Nota {
synchronized void esperar () {
try {
wait();
}
catch (InterruptedException e ){
;
}
}
synchronized void dar (){
notifyAll();
}
}
El resultado que se obtiene es el
siguiente:
Javi Esperado su nota Jose Esperado
su nota Voy a poner la nota Javi recibio su nota Jose recibio su nota
Expliquemos la manera en que se
desarrolla el programa. Una vez que se han generado los threads para
los alumnos y el profesor, los alumnos ejecutan el método
na.esperar() por lo que llaman al método wait() y quedan
esperando a que el profesor ponga la nota y llame al método
notify(). Esto lo realiza en el método na.dar().
Obsérvese que no se ha
ejecutado notify() sino notifyAll(). Si se hubiera ejecutado el
primero se habría desbloqueado a un thread, a un único
alumno, y el otro quedaría esperando. Puesto que lo que hay que
hacer es desbloquear más de un thread se utiliza el método
notifyAll().
Los threads Alumnos, una vez que han
sido desbloqueados por el profesor ordenadamente, reciben su nota, es
decir, continúan su ejecución en la línea
siguiente de código.
EL EJEMPLO CLÁSICO DEL
CONSUMIDOR Y PRODUCTOR
El ejemplo clásico en el tema
de la comunicación entre dos threads es el del consumidor y el
productor. Un thread productor genera un mensaje y lo pone en una
cola. Mientras otro thread, el consumidor, recoge ese mensaje y lo
visualiza. Tenemos un productor que tiene una cola limitada para
generar mensajes y para desarrollar un ejemplo claro, haremos que el
consumidor sea más lento que el productor, por lo que éste
a veces tendrá que parar hasta que el consumidor acabe con
algunos mensajes de la cola y se lo comunique .
Por tanto, tendremos un thread
productor que genera una serie de mensajes, parando si llena una cola.
Cada vez que genera un mensaje se lo notifica al consumidor y continúa
con su trabajo, generando más mensajes.
Por el otro lado, el consumidor se
encarga de recibir los mensajes y mostrarlos, por lo que dormirá
mientras no tenga nada que recibir. Ahora bien, ¿qué
ocurre cuando el productor esta durmiendo debido a que el consumidor
es muy lento?. Pasemos a ver el código de ejemplo: import java.util.Vector;
import java.util.Date;
class productor extends Thread {
static final int MAXELEM=1;
private Vector mensajes = new Vector();
public void run() {
try {
while ( true ) {
crearMensaje(); //genera mensaje
sleep ( 1000 ); // a dormir
}
}
catch ( InterruptedException e ) { }
}
private synchronized void crearMensaje()
throws InterruptedException
{
// Mientras que la cola este llena, a esperar ...
while ( mensajes.size() == MAXELEM )
wait();
// genero el mensaje, lo añado a la cola
mensajes.addElement ( new Date().toString() );
// Aviso al consumidor de que hay un nuevo mensaje
notify();
}
// Atencion, este método es llamado SOLO por el
CONSUMIDOR
public synchronized String recibirMensaje()
throws InterruptedException
{
// Mientras no haya nada que recoger ...
dormir ...
while ( mensajes.size() == 0 )
wait();
// Eliminamos el mensaje leido y lo devolvemos
String cadMensaje = (String)
mensajes.firstElement();
mensajes.removeElement( cadMensaje );
// Aviso al productor que se ponga a trabajar
si se durmió ya que puede añadir un elemento
notify();
return cadMensaje;
}
}
class consumidor extends Thread {
productor elProductor ;
consumidor ( productor unProductor ) {
elProductor = unProductor ;
}
public void run () {
try {
while ( true ) {
String cadMensaje= elProductor.recibirMensaje();
System.out.println ( " Recibido del productor "
+ cadMensaje );
sleep( 2000 ); // Consumidor a dormir ...
}
}
catch ( InterruptedException e ) { }
}
public static void main ( String args[] ) {
productor miProductor = new productor();
miProductor.start();
new consumidor( miProductor ).start();
}
}
Ejecutamos el código y
obtenemos lo siguiente:
Recibido del productor Sat Jun 27
18:34:51 GMT+02:00 1998
Recibido del productor Sat Jun 27
18:34:52 GMT+02:00 1998
Recibido del productor Sat Jun 27
18:34:53 GMT+02:00 1998
Recibido del productor Sat Jun 27
18:34:55 GMT+02:00 1998
Analicemos el código. El código
del productor consta de una cola de mensajes y se dedica a crear
mensajes y almacenarlos. Tras esto se pone a dormir. El método
crearMensaje() se encarga de la creación de los mismos,
comprobando si ya la cola de mensajes ya está llena, ya que si
no tendría que bloquearse al no poder crear más
mensajes. Este bloqueo estaría activo hasta que el consumidor
avisara al productor de que no tiene nada que recoger. Además,
el método almacena el mensaje y comunica el consumidor que ya
tiene la posibilidad de recoger un mensaje nuevo.
El método recibirMensaje()
que es invocado por el consumidor, nunca por el productor, se encarga
de la tarea contraria. En caso de que no haya ningún mensaje,
el consumidor espera, por lo que se bloquea llamando a wait(). Una vez
que hay un mensaje y el productor lo ha desbloqueado, lo recoge y lo
elimina de la cola del productor. A su vez, siempre que elimina un
elemento notifica al productor que despierte por si está
durmiendo y así éste se pone a generar nuevos mensajes.
Finaliza visualizándolo y haciendo una pausa, ya que hemos
hecho que el productor sea más veloz que el consumidor.
Vamos a añadir un segundo
consumidor al programa, para observar qué ocurre. Cambiamos la
clase consumidor de forma que contenga el siguiente código y
modificamos el método crearMensaje del productor para que su
notificación sea una llamada al método notifyAll(): private synchronized void crearMensaje() throws InterruptedException
{
// Mientras que la cola este llena, a esperar ...
while ( mensajes.size() == MAXELEM )
wait();
// genero el mensaje, lo añado a la cola
mensajes.addElement ( new Date().toString() );
// Aviso a los consumidores de que hay un
nuevo mensaje
notifyAll();
}
class consumidor extends Thread {
productor elProductor ;
String nombre;
consumidor ( String pNombre, productor
unProductor ) {
elProductor = unProductor ;
nombre = pNombre;
}
public void run () {
try {
while ( true ) {
String cadMensaje= elProductor.recibirMensaje();
System.out.println ( nombre + " recibe " +
cadMensaje );
sleep( 2000 ); // Consumidor a dormir ...
}
}
catch ( InterruptedException e ) { }
}
public static void main ( String args[] ) {
productor miProductor = new productor();
miProductor.start();
new consumidor( "consumidor1", miProductor ).start();
new consumidor( "consumidor2", miProductor ).start();
}
}
La ejecución muestra la
siguiente salida por pantalla tal y como la exponemos en el código
que describimos a continuación:
consumidor1 recibe Sat Jun 27
19:56:40 GMT+02:00 1998
consumidor2 recibe Sat Jun 27
19:56:41 GMT+02:00 1998
consumidor1 recibe Sat Jun 27
19:56:42 GMT+02:00 1998
consumidor2 recibe Sat Jun 27
19:56:43 GMT+02:00 1998
Como se observa se alternan
ordenadamente los dos consumidores como resultado de la llamada
sleep() en los distintos métodos. Es un buen ejercicio
comprobar qué sucede si se eliminan las llamadas a sleep(), ya
que según la implementación Java que estemos utilizando
observaremos diferentes resultados.
Además, se puede comprobar
que no es necesaria modificación alguna sobre el método
crearMensaje() del productor, es decir, la sentencia notifyAll() puede
ser también notify() ya que si un consumidor está
esperando a que aparezca un mensaje en la cola, no es posible que el
productor este esperando simultáneamente porque la cola esté
llena.
Como se ha detallado, el uso de
synchronized y el par notify/wait permite a los thread un tratamiento
correcto de los datos compartidos, es decir, una sincronización
de acceso y tratamiento. Como hemos visto, synchronized resulta más
conveniente para controlar el acceso simultaneo a un mismo método
de un objeto. El par notify/wait permite a los threads una
sincronización más definida, además de unos
mecanismos de bloqueo basados en la espera y notificación.
UN THREAD ESPECIAL, EL THREAD "DAEMON"
Un thread Daemon (demonio) es un
thread de baja prioridad que consta de un bucle infinito y se suele
utilizar para prestar servicios cuando se necesite, por ejemplo,
refresco de memoria, mostrar imágenes, etc. Java utiliza un
thread daemon, el recolector de basura para realizar todas las
gestiones sobre la creación y destrucción de los objetos
en memoria.
Un thread demonio se declara de la
siguiente manera: Thread.setDaemon();
y se puede comprobar si un thread es
tipo "demonio" mediante el siguiente método: isDaemon();
La diferencia entre los threads
normales y los demonios es que Java no espera la muerte de los
demonios para detener su ejecución. Los threads normales
impiden al núcleo de Java parar el programa principal hasta que
estos no mueren bien por la llamada a stop() o por la terminación
normal del thread. En el caso de los demonios, Java los elimina cuando
comprueba que únicamente quedan threads de del tipo demonios.
Su ventaja consiste en que no es necesario controlar su finalización,
de tal forma que se evitan los clásicos procedimientos de
pararCopiar(), detenerSpool(), etc.
Otra característica consiste
en que cualquier thread que se crea desde otro que es daemon también
es un thread demonio.
El siguiente ejemplo demuestra la
creación de threads demonios: import java.io.*;
class demonio extends Thread {
private int MAXELEM = 10 ;
private Thread [] t = new Thread [MAXELEM];
public demonio () {
setDaemon(true);
start();
}
public void run() {
for ( int i =0 ; i < MAXELEM ; i ++)
t[i] = new demonioHijo(i);
for ( int i =0 ; i < MAXELEM ; i ++) {
System.out.println ( " t[ "+i+ "]
is.Daemon() = " + t[i].isDaemon() );
}
while (true) yield();
}
}
class demonioHijo extends Thread {
public demonioHijo ( int i ) {
System.out.println ( " demonioHijo " + i
+ " iniciado ");
start();
}
public void run() {
while (true) yield();
}
}
public class misdemonios {
public static void main ( String [] args) throws
IOException
{
Thread d = new demonio();
System.out.println ( "d.isDaemon() " + d.isDaemon()
System.in.read();
);
}
CONCLUSIONES
Como se ha podido comprobar a lo
largo de este artículo, el lenguaje Java nos permite unos
sencillos y efectivos mecanismos de sincronización y comunicación
entre threads, que van desde el uso de la palabra clave synchronized
hasta el par wait/notify.
Gracias a estos métodos se
puede controlar el desarrollo de aplicaciones que utilicen recursos
compartidos entre múltiples threads. Además hemos
aprendido a desarrollar los threads "demonios", tan comunes
en lenguajes y sistemas operativos, con el fin de tener la posibilidad
de ofrecer servicios a otros threads. |