Threads

En esta entrada vamos a ver cómo se definen los threads en Android, cómo se les da trabajo a realizar, cómo podemos lanzarlos, pararlos, planificarlos, etc. y veremos también la forma que desde un thread podemos interactuar con el interfaz de usuario. Repasaremos previamente cómo se definen y gestionan los threads en Java. Trabajar con Threads es una de las formas que tenemos para lanzar procesos en segundo plano, existen otras que veremos en otros artículos.

Un thread se podría definir como el camino que sigue la ejecución de un programa. La máquina virtual Java permite a la aplicación tener múltiples threads de ejecución ejecutándose concurrentemente, “a la vez”. Concurrencia significa ejecución en paralelo de tareas. Ejecutar varias tareas simultaneamente es uno de los motivos por los que utilizamos los threads. Como Android sigue un modelo de thread unico para su interfaz de usuario, tendremos que crear diferentes threads para realizar nuestras tareas y “postear” o comunicar el resultado al main thread donde se actualizará el estado del interfaz de usuario. Revisar el artículo Multi-threading en Android.

Vamos a realizar un breve repaso de cómo se declaran y usan los threads en Java (sin Android), para posteriormente ver cómo lo hacemos en Android.

En Java

Un Thread en Java se crea asi:

Thread miThread = new Thread();

Para lanzar la ejecución de un Thread se llama a su método .start()

miThread.start();

En este ejemplo no hemos especificado el código que tenemos que ejecutar en el nuevo Thread.  Terminará nada más comenzar pues no ejecuta nada.

Hay dos formas de especificar el código que debe ejecutar un Thread.

  • Crear una subclase de Thread y sobreescribir su método .run().
  • Pasar en el constructor del Thread un objeto que implemente la interfaz Runanble.

El método run() es el que se ejecuta tras llamar al método start() del Thread.

public class MyThread extends Thread {
    public void run(){
       System.out.println("MyThread running");
    }
  }

Para crear y lanzar el thread:

MyThread myThread = new MyThread();
myTread.start();

El método start() termina tan pronto como el thread arranca, no espera a que el método run() termine.

Se puede crear una subclase anónima de un thread asi:

Thread thread = new Thread(){
    public void run(){
      System.out.println("Thread Running");
    }
  }
thread.start();

Un Runnable es un trozo de código que implementa el interfaz Runnable, por lo que tiene un método .run()

Tenemos que redefinir (@overridde) el metodo .run() de nuestro objeto Runnable . En el siguiente fragmento de código creamos una clase que implementa el interfaz runnable, en su método .run() colocamos el código que queremos que se ejecute.

public class MyRunnable implements Runnable {
    public void run(){
       System.out.println("MyRunnable running");
    }
  }

Para que el método run() del Runnable sea ejecutado por un Thread hay que pasarlo en el constructor del Thread.

Thread thread = new Thread(new MyRunnable());
thread.start();

En el siguiente código, usando una implementación anónima de un Runnable:

Runnable myRunnable = new Runnable(){
     public void run(){
        System.out.println("Runnable running");
     }
   }
Thread thread = new Thread(myRunnable);
thread.start();

Threads con nombre, Named Threads.

Se puede dar un nombre a un Thread para distinguirlo de otros, lo haremos pasando el nombre como cadena de texto en el constructor del Thread. El nombre de un Thread se puede obtener con el método getName() de la clase Thread.

Thread thread = new Thread("New Thread") {
      public void run(){
        System.out.println("run by: " + getname());
      }
   };
thread.start();
System.out.println(thread.getName());

También se puede pasar el nombre cuando se pasa un runnable, en este caso se pasan dos parámetro en el constructor del Thread, el runnable y el nombre.

MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable, "New Thread");
thread.start();
System.out.println(thread.getName());

Pero tenemos un problema para acceder al nombre del thread desde el código del runnable. Como la clase MyRunnable() no es subclase de Thread no tiene acceso al método getName() de la clase Thread. Se puede obtener una referencia al thread actual, desde el Runnable, con Thread.currentThread() y así acceder al método:

String threadName = Thread.currentThread().getName();

En Android

En Android es básicamente igual. Podemos crear un Thread y un Runnable, que será pasado a un thread para su ejecución.

class Test extends Thread {
    @Override
    public void run() {
        Log.d("Threading", "Test class thread is --> "+Thread.currentThread().getName());
    }
}

El interfaz Runnable deberá ser implementado por cualquier clase cuyas instancias vayan a ser ejecutadas por un thread. Definiremos su código en el método run() de la interfaz.

Cualquier thread en Android está controlado por la clase java.lang.Thread

class Test implements Runnable {
    @Override
    public void run() {
        Log.d("Test", "Test class thread is --> "+Thread.currentThread().getName());
    }
}

Thread Life Cycle

Un thread Java pasa por un conjunto de estados. El ciclo de vida del thead, es decir, el paso entre sus estados, esttá controlado por la Java Virtual Machine (JVM). Los estados por los que puede pasar un thread están definidos en la enumeración Thread.state. Podemos obtener el estado de un thread llamando a la función miembro .getState(). Hay que tener en cuenta que el estado del thread puede cambiar tras la llamada a .getState() y que por tanto el estado actual (un instante después) difiera del reportado por la función.

Estos estados, que se muestran en la imagen con sus transiciones, son:

  • NEW: Cuando un thread es creado pero todavía no se ha ejecutado (el método start() no se ha invocado todavía)
  • RUNNABLE: Cuando el método start() se ha invocado. El thread entra en el estado de runnable y su método .run() se está ejecutando. Un thread puede acabar de llegar desde un estado de bloqueo o espera (blocked, waiting) por lo que puede que todavía no haya sido seleccionado por el planificador, por eso se le llama runnable en vez de “running”. Podríamos verlo como dos estados “Listo” y “Ejecutando” pero Android mantiene sólo un estado.
  • BLOCKED: Cuando un thread intenta adquirir un lock intrínseco que esta actualmente retenido o asignado a otro thread, por lo que se bloquea a la espera de la liberación de dicho lock. Cuando todos los otros hilos han abandonado el bloqueo y el thread scheduler ha permitido le asigna el lock, el hilo se desbloquea y entra en el estado runnable.
  • WAITING: Un thread entra en el estado de waiting cuando espera una notificación de otro thread, bien por la ejecución de Object.wait() o de Thread.join(). Un thread también puede entrar en este estado si está esperando por un lock o una condition del package java.util.concurrent.package (revisar métodos de sincronización e IPC en sistemas operativos). Cuando otro thread llama a .notify() o .notifyAll del objeto o bien a los métodos signal() o signalAll() de una condición, el proceso puede continuar y pasará al estado de runnable.
  • TIMED_WAITING: Un thread entra en este estado si un método con un timeout es llamado, por ejemplo sleep(), wait(), join(), Lock.tryLock() y Condiditon.await(). El thread sale de este estado cuando el timeout expira o le llega la notificación relacionada.
  • TERMINATED: Un thread entra en este estado cuando ha completado su ejecución. El thread termina por una de estos dos motivos:
    • Terminación normal del método .run()
    • Terminación abrupta del método .run() debido a una excepción no capturada.

Podemos realizar cualquier operación dentro de un thread, excepto actualizar los elementos del interfaz de usuario que corren en otro thread, el main thread. Para poder modificarlos debemos utilizar un handler o bien el método runOnUIThread(). En el siguiente ejemplo vemos cómo se pueden actualizar vistas del interfaz de usuario:

private void yourMethodName(){
new Thread(new Runnable() {
    @Override
    public void run() {
            try {
             yourActivity.runOnUiThread(new Runnable() {
             @Override
             public void run() {
                    txtview.setText("some value");
                    edittext.setText("some new value");
             }
             });
            }catch (Exception e) {
                  //print the error here
            }
      }
 }).start();
}

runOnUiThread() ejecuta la acción especificada en el UI thread. Si el thread actual es el UI thread, la acción se ejecuta inmediatamente, si no, la acción es enviada (posted) a la cola de eventos del UI thread.

Es decir:

  • El trabajo se realiza en un worker-Thread
  • Las actualizaciones de interfaz se realizan en el UI Thread

Android proporciona varios métodos que garantiza que son ejecutados en el UI Thread. Los métodos que tenemos para actualizar el UI Thread son los siguientes:

  • En la clase View el método post: boolean View.post(Runnable action)
  • En la clase Activity el método : void Activity.runOnUiThread(Runnable action)
  • En la clase Activity el método : public boolean postDelayed (Runnable action, long delayMillis)

Como vemos todos reciben un Runnable.

Para ver ejemplo un ejemplo de cómo se puede utilizar .post() para cargar una imagen, cuya descarga supuestamente tarda mucho, podéis acceder al código ThreadingSimplePost en el ejercicio ThreadingSimple.